From 9fcf5c07777006e5b26ebddd0a003dd82d4bd254 Mon Sep 17 00:00:00 2001 From: Denis Fokin Date: Wed, 5 Oct 2022 12:06:28 +0300 Subject: [PATCH 01/98] UTBotFamily --- .gitignore | 3 +- settings.gradle | 26 +- utbot-analytics/build.gradle | 2 + utbot-cli-js/build.gradle | 84 ++ utbot-cli-js/src/README.md | 72 ++ .../kotlin/org/utbot/cli/js/Application.kt | 35 + .../org/utbot/cli/js/JsCoverageCommand.kt | 153 +++ .../utbot/cli/js/JsGenerateTestsCommand.kt | 121 +++ .../org/utbot/cli/js/JsRunTestsCommand.kt | 60 ++ .../main/kotlin/org/utbot/cli/js/JsUtils.kt | 15 + utbot-cli-js/src/main/resources/log4j2.xml | 16 + .../src/main/resources/version.properties | 2 + utbot-cli-python/build.gradle | 85 ++ .../utbot/cli/language/python/Application.kt | 33 + .../org/utbot/cli/language/python/Optional.kt | 25 + .../python/PythonGenerateTestsCommand.kt | 279 +++++ .../language/python/PythonRunTestsCommand.kt | 84 ++ .../src/main/resources/log4j2.xml | 16 + .../src/main/resources/version.properties | 2 + .../main/kotlin/org/utbot/cli/Application.kt | 6 +- .../main/kotlin/org/utbot/common/FileUtil.kt | 23 +- .../org/utbot/framework/plugin/api/Api.kt | 14 +- .../utbot/framework/plugin/api/CoverageApi.kt | 2 +- .../utbot/framework/plugin/api/go/GoApi.kt | 75 ++ .../utbot/framework/plugin/api/js/JsApi.kt | 160 +++ .../framework/plugin/api/js/util/JsIdUtil.kt | 55 + .../framework/plugin/api/python/PythonApi.kt | 179 ++++ .../framework/plugin/api/python/PythonTree.kt | 134 +++ .../plugin/api/python/util/PythonIdUtils.kt | 16 + .../plugin/api/python/util/PythonUtils.kt | 16 + .../utbot/framework/plugin/api/util/IdUtil.kt | 6 +- .../org/utbot/engine/ValueConstructor.kt | 2 + .../assemble/AssembleModelGenerator.kt | 2 + .../org/utbot/framework/codegen/Domain.kt | 19 +- .../org/utbot/framework/codegen/Keywords.kt | 10 +- .../framework/codegen/model/CodeGenerator.kt | 18 +- .../model/constructor/CgMethodTestSet.kt | 31 +- .../model/constructor/TestClassContext.kt | 2 +- .../constructor/builtin/UtilMethodBuiltins.kt | 2 +- .../model/constructor/context/CgContext.kt | 24 +- .../model/constructor/name/CgNameGenerator.kt | 16 +- .../tree/CgCallableAccessManager.kt | 2 +- .../constructor/tree/CgFieldStateManager.kt | 2 +- .../constructor/tree/CgMethodConstructor.kt | 49 +- .../tree/CgTestClassConstructor.kt | 18 +- .../constructor/tree/CgVariableConstructor.kt | 7 +- .../constructor/tree/MockFrameworkManager.kt | 5 +- .../constructor/tree/TestFrameworkManager.kt | 7 +- .../util/CgStatementConstructor.kt | 2 +- .../constructor/util/ConstructorUtils.kt | 10 +- .../framework/codegen/model/tree/CgElement.kt | 5 +- .../codegen/model/util/DependencyPatterns.kt | 4 + .../framework/codegen/model/util/DslUtil.kt | 1 + .../framework/codegen/model/util/TreeUtil.kt | 2 +- .../model/visitor/CgAbstractRenderer.kt | 17 +- .../codegen/model/visitor/CgKotlinRenderer.kt | 2 +- .../model/visitor/CgRendererContext.kt | 9 +- .../codegen/model/visitor/CgVisitor.kt | 3 + .../codegen/model/visitor/UtilMethods.kt | 20 + .../concrete/MockValueConstructor.kt | 8 + .../framework/concrete/UtModelConstructor.kt | 2 +- .../fields/ExecutionStateAnalyzer.kt | 6 + .../framework/minimization/Minimization.kt | 7 + .../framework/plugin/api/CodeLanguage.kt | 69 ++ .../framework/plugin/api/JavaCodeLanguage.kt | 51 + .../plugin/api/KotlinCodeLanguage.kt | 49 + .../plugin/api/utils/CodeLanguageUtils.kt | 14 + .../org/utbot/framework/util/TestUtils.kt | 2 + .../TestCodeGeneratorPipeline.kt | 1 + .../utbot/fuzzer/FuzzedMethodDescription.kt | 29 +- .../main/kotlin/org/utbot/fuzzer/Fuzzer.kt | 6 + .../providers/ConstantsModelProvider.kt | 2 +- utbot-go/README.md | 117 ++ utbot-go/build.gradle | 33 + utbot-go/docs/DEVELOPERS_GUIDE.md | 13 + utbot-go/docs/FUTURE_PLANS.md | 13 + .../install-intellij-plugin-from-disk.png | Bin 0 -> 25500 bytes utbot-go/samples/primitive_types.go | 284 +++++ .../kotlin/org/utbot/go/api/GoTypesApi.kt | 24 + .../utbot/go/api/GoUtExecutionResultsApi.kt | 18 + .../org/utbot/go/api/GoUtFunctionApi.kt | 49 + .../kotlin/org/utbot/go/api/GoUtModelsApi.kt | 77 ++ .../org/utbot/go/api/util/GoTypesApiUtil.kt | 96 ++ .../utbot/go/api/util/GoUtModelsApiUtil.kt | 29 + .../go/executor/GoFuzzedFunctionsExecutor.kt | 170 +++ ...edFunctionsExecutorCodeGenerationHelper.kt | 234 ++++ .../utbot/go/executor/RawExecutionResults.kt | 11 + .../kotlin/org/utbot/go/fuzzer/GoFuzzer.kt | 33 + .../providers/GoConstantsModelProvider.kt | 58 + .../providers/GoPrimitivesModelProvider.kt | 142 +++ .../GoStringConstantModelProvider.kt | 54 + .../go/gocodeanalyzer/AnalysisResults.kt | 21 + .../go/gocodeanalyzer/AnalysisTargets.kt | 5 + .../go/gocodeanalyzer/GoSourceCodeAnalyzer.kt | 122 +++ .../AbstractGoUtTestsGenerationController.kt | 86 ++ .../utbot/go/logic/GoTestCasesGenerator.kt | 32 + .../GoCodeGenerationUtil.kt | 42 + .../simplecodegeneration/GoFileCodeBuilder.kt | 36 + .../GoTestCasesCodeGenerator.kt | 202 ++++ .../org/utbot/go/util/GoFuzzedValueUtil.kt | 6 + .../main/kotlin/org/utbot/go/util/JsonUtil.kt | 24 + .../org/utbot/go/util/ProcessExecutionUtil.kt | 27 + .../analysis_results.go | 32 + .../analysis_targets.go | 10 + .../go_source_code_analyzer/analyzer_core.go | 143 +++ .../resources/go_source_code_analyzer/main.go | 77 ++ utbot-intellij-js/build.gradle.kts | 74 ++ .../plugin/language/js/JsDialogProcessor.kt | 198 ++++ .../plugin/language/js/JsDialogWindow.kt | 299 ++++++ .../plugin/language/js/JsLanguageAssistant.kt | 142 +++ .../plugin/language/js/JsTestsModel.kt | 29 + utbot-intellij-python/build.gradle.kts | 72 ++ .../plugin/language/python/IterationUtil.kt | 13 + .../language/python/PythonDialogProcessor.kt | 266 +++++ .../language/python/PythonDialogWindow.kt | 163 +++ .../python/PythonLanguageAssistant.kt | 78 ++ .../language/python/PythonTestsModel.kt | 34 + .../generator/CodeGenerationController.kt | 10 +- .../intellij/plugin/language/JavaLanguage.kt | 164 +++ .../plugin/models/GenerateTestsModel.kt | 26 +- .../plugin/ui/GenerateTestsDialogWindow.kt | 5 +- .../plugin/ui/actions/GenerateTestsAction.kt | 231 +--- .../intellij/plugin/ui/utils/AndroidUtils.kt | 41 + .../src/main/resources/META-INF/plugin.xml | 9 +- .../src/main/resources/META-INF/withGo.xml | 3 + .../src/main/resources/META-INF/withJS.xml | 4 + .../src/main/resources/META-INF/withJava.xml | 3 + .../main/resources/META-INF/withKotlin.xml | 3 + .../src/main/resources/META-INF/withLang.xml | 3 + .../main/resources/META-INF/withPython.xml | 4 + utbot-js/build.gradle.kts | 55 + utbot-js/docs/CLI.md | 70 ++ utbot-js/samples/bitOperators.js | 31 + utbot-js/samples/commonIfStatement.js | 7 + utbot-js/samples/commonLoops.js | 27 + utbot-js/samples/commonRecursion.js | 19 + utbot-js/samples/commonString.js | 19 + .../samples/functionsThrowExceptionsInRow.js | 24 + .../samples/scenarioMultyClassNoTopLevel.js | 39 + utbot-js/samples/scenarioObjectParameter.js | 14 + utbot-js/samples/scenarioStaticMethod.js | 13 + utbot-js/samples/scenarioThrowError.js | 11 + .../src/main/kotlin/api/JsTestGenerator.kt | 487 +++++++++ .../main/kotlin/api/JsUtModelConstructor.kt | 66 ++ .../main/kotlin/codegen/JsCodeGenerator.kt | 86 ++ .../framework/codegen/JsCodeLanguage.kt | 60 ++ .../main/kotlin/framework/codegen/JsDomain.kt | 64 ++ .../tree/JsCgCallableAccessManager.kt | 50 + .../constructor/tree/JsCgMethodConstructor.kt | 133 +++ .../tree/JsCgStatementConstructor.kt | 270 +++++ .../tree/JsCgVariableConstructor.kt | 36 + .../tree/JsTestFrameworkManager.kt | 56 + .../constructor/util/ConstructorUtils.kt | 16 + .../model/constructor/visitor/CgJsRenderer.kt | 386 +++++++ utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt | 32 + .../providers/JsConstantsModelProvider.kt | 61 ++ .../providers/JsMultipleTypesModelProvider.kt | 77 ++ .../fuzzer/providers/JsObjectModelProvider.kt | 88 ++ .../providers/JsPrimitivesModelProvider.kt | 70 ++ .../fuzzer/providers/JsStringModelProvider.kt | 53 + .../providers/JsUndefinedModelProvider.kt | 39 + .../main/kotlin/parser/JsClassAstVisitor.kt | 26 + .../kotlin/parser/JsFunctionAstVisitor.kt | 32 + .../main/kotlin/parser/JsFuzzerAstVisitor.kt | 76 ++ .../src/main/kotlin/parser/JsParserUtils.kt | 29 + .../parser/JsToplevelFunctionAstVisitor.kt | 22 + .../main/kotlin/service/CoverageService.kt | 98 ++ .../src/main/kotlin/service/ServiceContext.kt | 10 + .../src/main/kotlin/service/TernService.kt | 210 ++++ .../main/kotlin/settings/JsExportsSettings.kt | 11 + .../settings/JsTestGenerationSettings.kt | 19 + .../main/kotlin/utils/JsClassConstructors.kt | 74 ++ utbot-js/src/main/kotlin/utils/JsCmdExec.kt | 40 + utbot-js/src/main/kotlin/utils/MethodTypes.kt | 8 + .../src/main/kotlin/utils/PathResolver.kt | 12 + utbot-js/src/main/kotlin/utils/ValueUtil.kt | 45 + utbot-python/README.md | 48 + utbot-python/build.gradle.kts | 42 + utbot-python/docs/CLI.md | 101 ++ utbot-python/docs/docs.md | 54 + utbot-python/samples/.gitignore | 3 + .../generated_tests__arithmetic.py | 40 + .../generated_tests__deep_equals.py | 87 ++ .../cli_utbot_tests/generated_tests__deque.py | 35 + .../cli_utbot_tests/generated_tests__dicts.py | 30 + .../generated_tests__dummy_with_eq.py | 39 + .../generated_tests__dummy_without_eq.py | 36 + .../cli_utbot_tests/generated_tests__graph.py | 36 + .../generated_tests__list_of_datetime.py | 32 + .../cli_utbot_tests/generated_tests__lists.py | 21 + .../generated_tests__longest_subsequence.py | 25 + .../generated_tests__matrix.py | 41 + .../generated_tests__primitive_types.py | 40 + .../generated_tests__quick_sort.py | 30 + .../generated_tests__test_coverage.py | 35 + .../generated_tests__type_inference.py | 20 + .../generated_tests__using_collections.py | 23 + utbot-python/samples/easy_samples/.gitignore | 3 + .../samples/easy_samples/corner_cases.py | 16 + .../samples/easy_samples/deep_equals.py | 74 ++ .../samples/easy_samples/empty_file.py | 0 .../samples/easy_samples/fully_annotated.py | 78 ++ utbot-python/samples/easy_samples/general.py | 193 ++++ .../samples/easy_samples/sample_classes.py | 21 + utbot-python/samples/generate_test_samples.sh | 24 + utbot-python/samples/run_test_samples.sh | 24 + utbot-python/samples/samples.md | 27 + utbot-python/samples/samples/arithmetic.py | 17 + utbot-python/samples/samples/deep_equals.py | 22 + utbot-python/samples/samples/deque.py | 8 + utbot-python/samples/samples/dicts.py | 29 + utbot-python/samples/samples/dummy_with_eq.py | 9 + .../samples/samples/dummy_without_eq.py | 3 + utbot-python/samples/samples/graph.py | 42 + .../samples/samples/list_of_datetime.py | 10 + utbot-python/samples/samples/lists.py | 25 + .../samples/samples/longest_subsequence.py | 27 + utbot-python/samples/samples/matrix.py | 69 ++ .../samples/samples/primitive_types.py | 11 + utbot-python/samples/samples/quick_sort.py | 32 + utbot-python/samples/samples/test_coverage.py | 12 + .../samples/samples/type_inference.py | 16 + .../samples/samples/using_collections.py | 15 + .../kotlin/org/utbot/python/PythonEngine.kt | 158 +++ .../org/utbot/python/PythonEvaluation.kt | 125 +++ .../utbot/python/PythonTestCaseGenerator.kt | 166 +++ .../python/PythonTestGenerationProcessor.kt | 277 +++++ .../kotlin/org/utbot/python/UTPythonAPI.kt | 36 + .../org/utbot/python/code/ArgInfoCollector.kt | 542 ++++++++++ .../utbot/python/code/ClassInfoCollector.kt | 64 ++ .../kotlin/org/utbot/python/code/CodeGen.kt | 422 ++++++++ .../python/code/KlaxonPythonTreeParser.kt | 102 ++ .../org/utbot/python/code/PythonASTParser.kt | 420 ++++++++ .../org/utbot/python/code/PythonCodeAPI.kt | 188 ++++ .../framework/codegen/PythonCodeLanguage.kt | 61 ++ .../codegen/model/PythonCodeGenerator.kt | 74 ++ .../framework/codegen/model/PythonDomain.kt | 85 ++ .../framework/codegen/model/PythonImports.kt | 86 ++ .../constructor/name/PythonCgNameGenerator.kt | 57 + .../tree/PythonCgCallableAccessManager.kt | 51 + .../tree/PythonCgMethodConstructor.kt | 343 ++++++ .../tree/PythonCgStatementConstructor.kt | 237 +++++ .../tree/PythonCgTestClassConstructor.kt | 113 ++ .../tree/PythonCgVariableConstructor.kt | 39 + .../tree/PythonTestFrameworkManager.kt | 140 +++ .../constructor/util/ConstructorUtils.kt | 29 + .../constructor/visitor/CgPythonRenderer.kt | 468 ++++++++ .../constructor/visitor/CgPythonVisitor.kt | 18 + .../codegen/model/tree/CgPythonElement.kt | 101 ++ .../python/providers/ConstantModelProvider.kt | 44 + .../providers/DefaultValuesModelProvider.kt | 24 + .../providers/GeneralPythonModelProvider.kt | 51 + .../python/providers/GenericModelProvider.kt | 103 ++ .../python/providers/InitModelProvider.kt | 41 + .../python/providers/OptionalModelProvider.kt | 35 + .../python/providers/UnionModelProvider.kt | 28 + .../utbot/python/typing/FindAnnotations.kt | 204 ++++ .../utbot/python/typing/GenericAnnotations.kt | 90 ++ .../utbot/python/typing/MypyAnnotations.kt | 149 +++ .../python/typing/PythonTypeCollector.kt | 231 ++++ .../org/utbot/python/typing/StubFileFinder.kt | 150 +++ .../org/utbot/python/typing/StubFileReader.kt | 24 + .../utbot/python/typing/StubFileStructures.kt | 112 ++ .../python/utils/AnnotationNormalizer.kt | 92 ++ .../kotlin/org/utbot/python/utils/Cleaner.kt | 22 + .../python/utils/PriorityCartesianProduct.kt | 41 + .../org/utbot/python/utils/ProcessUtils.kt | 43 + .../utbot/python/utils/RequirementsUtils.kt | 37 + .../org/utbot/python/utils/StringUtils.kt | 48 + .../python/utils/TemporaryFileManager.kt | 49 + .../src/main/resources/check_requirements.py | 7 + .../normalize_annotation_from_project.py | 50 + .../main/resources/preprocessed_values.json | 997 ++++++++++++++++++ .../main/resources/python_tree_serializer.py | 206 ++++ .../src/main/resources/requirements.txt | 4 + .../src/main/resources/typeshed_stub.py | 322 ++++++ utbot-python/todo.md | 62 ++ utbot-python/todo_refactoring.md | 57 + utbot-ui-commons/build.gradle.kts | 26 + .../language/agnostic/LanguageAssistant.kt | 42 + .../intellij/plugin/models/BaseTestModel.kt | 36 + .../utbot/intellij/plugin/ui/Notifications.kt | 0 .../CodeGenerationSettingItemRenderer.kt | 0 .../TestFolderComboWithBrowseButton.kt | 17 +- .../intellij/plugin/ui/utils/ErrorUtils.kt | 0 .../intellij/plugin/ui/utils/ModuleUtils.kt | 8 +- .../intellij/plugin/ui/utils/RootUtils.kt | 4 +- 287 files changed, 18788 insertions(+), 396 deletions(-) create mode 100644 utbot-cli-js/build.gradle create mode 100644 utbot-cli-js/src/README.md create mode 100644 utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt create mode 100644 utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt create mode 100644 utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt create mode 100644 utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt create mode 100644 utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt create mode 100644 utbot-cli-js/src/main/resources/log4j2.xml create mode 100644 utbot-cli-js/src/main/resources/version.properties create mode 100644 utbot-cli-python/build.gradle create mode 100644 utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt create mode 100644 utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt create mode 100644 utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt create mode 100644 utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt create mode 100644 utbot-cli-python/src/main/resources/log4j2.xml create mode 100644 utbot-cli-python/src/main/resources/version.properties create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/go/GoApi.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/util/JsIdUtil.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonApi.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonTree.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonIdUtils.kt create mode 100644 utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonUtils.kt create mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt create mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt create mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt create mode 100644 utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt create mode 100644 utbot-go/README.md create mode 100644 utbot-go/build.gradle create mode 100644 utbot-go/docs/DEVELOPERS_GUIDE.md create mode 100644 utbot-go/docs/FUTURE_PLANS.md create mode 100644 utbot-go/docs/images/install-intellij-plugin-from-disk.png create mode 100644 utbot-go/samples/primitive_types.go create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutor.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutorCodeGenerationHelper.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/executor/RawExecutionResults.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/GoFuzzer.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantsModelProvider.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesModelProvider.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStringConstantModelProvider.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/util/GoFuzzedValueUtil.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt create mode 100644 utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go create mode 100644 utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go create mode 100644 utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go create mode 100644 utbot-go/src/main/resources/go_source_code_analyzer/main.go create mode 100644 utbot-intellij-js/build.gradle.kts create mode 100644 utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt create mode 100644 utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt create mode 100644 utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt create mode 100644 utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt create mode 100644 utbot-intellij-python/build.gradle.kts create mode 100644 utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IterationUtil.kt create mode 100644 utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt create mode 100644 utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt create mode 100644 utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt create mode 100644 utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt create mode 100644 utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt create mode 100644 utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/AndroidUtils.kt create mode 100644 utbot-intellij/src/main/resources/META-INF/withGo.xml create mode 100644 utbot-intellij/src/main/resources/META-INF/withJS.xml create mode 100644 utbot-intellij/src/main/resources/META-INF/withJava.xml create mode 100644 utbot-intellij/src/main/resources/META-INF/withKotlin.xml create mode 100644 utbot-intellij/src/main/resources/META-INF/withLang.xml create mode 100644 utbot-intellij/src/main/resources/META-INF/withPython.xml create mode 100644 utbot-js/build.gradle.kts create mode 100644 utbot-js/docs/CLI.md create mode 100644 utbot-js/samples/bitOperators.js create mode 100644 utbot-js/samples/commonIfStatement.js create mode 100644 utbot-js/samples/commonLoops.js create mode 100644 utbot-js/samples/commonRecursion.js create mode 100644 utbot-js/samples/commonString.js create mode 100644 utbot-js/samples/functionsThrowExceptionsInRow.js create mode 100644 utbot-js/samples/scenarioMultyClassNoTopLevel.js create mode 100644 utbot-js/samples/scenarioObjectParameter.js create mode 100644 utbot-js/samples/scenarioStaticMethod.js create mode 100644 utbot-js/samples/scenarioThrowError.js create mode 100644 utbot-js/src/main/kotlin/api/JsTestGenerator.kt create mode 100644 utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt create mode 100644 utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt create mode 100644 utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt create mode 100644 utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt create mode 100644 utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt create mode 100644 utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt create mode 100644 utbot-js/src/main/kotlin/parser/JsParserUtils.kt create mode 100644 utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt create mode 100644 utbot-js/src/main/kotlin/service/CoverageService.kt create mode 100644 utbot-js/src/main/kotlin/service/ServiceContext.kt create mode 100644 utbot-js/src/main/kotlin/service/TernService.kt create mode 100644 utbot-js/src/main/kotlin/settings/JsExportsSettings.kt create mode 100644 utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt create mode 100644 utbot-js/src/main/kotlin/utils/JsClassConstructors.kt create mode 100644 utbot-js/src/main/kotlin/utils/JsCmdExec.kt create mode 100644 utbot-js/src/main/kotlin/utils/MethodTypes.kt create mode 100644 utbot-js/src/main/kotlin/utils/PathResolver.kt create mode 100644 utbot-js/src/main/kotlin/utils/ValueUtil.kt create mode 100644 utbot-python/README.md create mode 100644 utbot-python/build.gradle.kts create mode 100644 utbot-python/docs/CLI.md create mode 100644 utbot-python/docs/docs.md create mode 100644 utbot-python/samples/.gitignore create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__deque.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__graph.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__lists.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py create mode 100644 utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py create mode 100644 utbot-python/samples/easy_samples/.gitignore create mode 100644 utbot-python/samples/easy_samples/corner_cases.py create mode 100644 utbot-python/samples/easy_samples/deep_equals.py create mode 100644 utbot-python/samples/easy_samples/empty_file.py create mode 100644 utbot-python/samples/easy_samples/fully_annotated.py create mode 100644 utbot-python/samples/easy_samples/general.py create mode 100644 utbot-python/samples/easy_samples/sample_classes.py create mode 100644 utbot-python/samples/generate_test_samples.sh create mode 100755 utbot-python/samples/run_test_samples.sh create mode 100644 utbot-python/samples/samples.md create mode 100644 utbot-python/samples/samples/arithmetic.py create mode 100644 utbot-python/samples/samples/deep_equals.py create mode 100644 utbot-python/samples/samples/deque.py create mode 100644 utbot-python/samples/samples/dicts.py create mode 100644 utbot-python/samples/samples/dummy_with_eq.py create mode 100644 utbot-python/samples/samples/dummy_without_eq.py create mode 100644 utbot-python/samples/samples/graph.py create mode 100644 utbot-python/samples/samples/list_of_datetime.py create mode 100644 utbot-python/samples/samples/lists.py create mode 100644 utbot-python/samples/samples/longest_subsequence.py create mode 100644 utbot-python/samples/samples/matrix.py create mode 100644 utbot-python/samples/samples/primitive_types.py create mode 100644 utbot-python/samples/samples/quick_sort.py create mode 100644 utbot-python/samples/samples/test_coverage.py create mode 100644 utbot-python/samples/samples/type_inference.py create mode 100644 utbot-python/samples/samples/using_collections.py create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt create mode 100644 utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt create mode 100644 utbot-python/src/main/resources/check_requirements.py create mode 100644 utbot-python/src/main/resources/normalize_annotation_from_project.py create mode 100644 utbot-python/src/main/resources/preprocessed_values.json create mode 100644 utbot-python/src/main/resources/python_tree_serializer.py create mode 100644 utbot-python/src/main/resources/requirements.txt create mode 100644 utbot-python/src/main/resources/typeshed_stub.py create mode 100644 utbot-python/todo.md create mode 100644 utbot-python/todo_refactoring.md create mode 100644 utbot-ui-commons/build.gradle.kts create mode 100644 utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt create mode 100644 utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt (100%) rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt (100%) rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt (88%) rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt (100%) rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt (99%) rename {utbot-intellij => utbot-ui-commons}/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt (97%) diff --git a/.gitignore b/.gitignore index 011d499004..2070e7fe6a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ target/ .idea/ .gradle/ *.log -*.rdgen \ No newline at end of file +*.rdgen +__pycache__/ diff --git a/settings.gradle b/settings.gradle index 7e7cd525dc..0f1ac44d76 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,20 +14,28 @@ include 'utbot-core' include 'utbot-framework' include 'utbot-framework-api' include 'utbot-intellij' -include 'utbot-sample' +//include 'utbot-sample' include 'utbot-fuzzers' -include 'utbot-junit-contest' -include 'utbot-analytics' -include 'utbot-analytics-torch' -include 'utbot-cli' +//include 'utbot-junit-contest' +//include 'utbot-analytics' +//include 'utbot-analytics-torch' +//include 'utbot-cli' +//include 'utbot-cli-python' +//include 'utbot-cli-js' include 'utbot-api' include 'utbot-instrumentation' -include 'utbot-instrumentation-tests' +//include 'utbot-instrumentation-tests' include 'utbot-summary' -include 'utbot-gradle' +//include 'utbot-gradle' include 'utbot-maven' -include 'utbot-summary-tests' -include 'utbot-framework-test' +//include 'utbot-summary-tests' +//include 'utbot-framework-test' include 'utbot-rd' +include 'utbot-python' +//include 'utbot-js' +//include 'utbot-go' +//include 'utbot-intellij-js' +include 'utbot-ui-commons' +include 'utbot-intellij-python' diff --git a/utbot-analytics/build.gradle b/utbot-analytics/build.gradle index 78257d605d..5d0486e263 100644 --- a/utbot-analytics/build.gradle +++ b/utbot-analytics/build.gradle @@ -1,3 +1,5 @@ +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle.kts" + configurations { mlmodels } diff --git a/utbot-cli-js/build.gradle b/utbot-cli-js/build.gradle new file mode 100644 index 0000000000..ffe61ddd1c --- /dev/null +++ b/utbot-cli-js/build.gradle @@ -0,0 +1,84 @@ +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 +} + + +//noinspection GroovyAssignabilityCheck +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework-api') + implementation project(':utbot-framework') + implementation project(':utbot-summary') + implementation project(':utbot-cli') + api project(':utbot-js') + + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4_platform_version + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: clikt_version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.json', name: 'json', version: '20220320' + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + dependsOn project(':utbot-framework').tasks.jar + dependsOn project(':utbot-summary').tasks.jar + dependsOn project(':utbot-js').tasks.jar + dependsOn project(':utbot-fuzzers').tasks.jar + + manifest { + attributes 'Main-Class': 'org.utbot.cli.js.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.js' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot JavaScript CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-js/src/README.md b/utbot-cli-js/src/README.md new file mode 100644 index 0000000000..4e1b8b86e1 --- /dev/null +++ b/utbot-cli-js/src/README.md @@ -0,0 +1,72 @@ +## Build + +.jar file can be built in GitHub Actions with script publish-plugin-and-cli-from-branch. + +## Requirements + +* NodeJs 10.0.0 or higher (available to download https://nodejs.org/en/download/) +* Java 11 or higher (available to download https://www.oracle.com/java/technologies/downloads/) +* Nyc 15.1.0 or higher (`> npm install -g nyc`) +* Mocha 10.0.0 or higher (`> npm install -g mocha`) + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + +This will generate tests for top-level functions from `file_with_sources.js`. + +Run generated tests: + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + +This will run generated tests from file or directory. + +Generate coverage report: + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + +This will generate coverage report from generated tests and print in `StdOut` + +## `generate_js` options + +- `-s, --source ` + + (required) Source code file for a test generation. +- `-c, --class ` + + If not specified, tests for top-level functions or single class are generated, otherwise for the specified class. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether test should be printed out to `StdOut` (default = false) +- `-t, --timeout ` + + Timeout for a single test case to generate in seconds (default = 5) + +## `run_js` options + +- `-f, --fileOrDir` + + (required) File or directory with tests. +- `-o, --output` + + Specifies output of .txt file for test framework result (If empty prints to `StdOut`) + +- `-t, --test-framework [mocha]` + + Test framework of tests to run. + +## `coverage_js` options + +- `-s, --source ` + + (required) File with tests to generate a report. + +- `-o, --output` + + Specifies output .json file for generated tests (If empty prints .json to `StdOut`) \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt new file mode 100644 index 0000000000..4b22841ec5 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/Application.kt @@ -0,0 +1,35 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess + +class UtBotCli : CliktCommand(name = "UnitTestBot JavaScript Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotCli().subcommands( + JsCoverageCommand(), + JsGenerateTestsCommand(), + JsRunTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt new file mode 100644 index 0000000000..88c7e17012 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsCoverageCommand.kt @@ -0,0 +1,153 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import mu.KotlinLogging +import org.json.JSONArray +import org.json.JSONObject +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import org.w3c.dom.Document +import org.w3c.dom.Element +import utils.JsCmdExec +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import javax.xml.parsers.DocumentBuilderFactory + +private val logger = KotlinLogging.logger {} + +class JsCoverageCommand : CliktCommand(name = "coverage_js", help = "Get tests coverage for the specified file") { + + private val testFile by option( + "-s", "--source", + help = "Target test file path" + ).required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val output by option( + "-o", "--output", + help = "Specifies output .json file for generated tests" + ).check("Must end with .json suffix") { + it.endsWith(".json") + } + + override fun run() { + val testFileAbsolutePath = makeAbsolutePath(testFile) + val workingDir = testFileAbsolutePath.substringBeforeLast(File.separator) + val coverageDataPath = "$workingDir${File.separator}coverage${File.separator}" + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + JsCmdExec.runCommand( + cmd = "nyc " + + "--report-dir=$coverageDataPath " + + "--reporter=\"clover\" " + + "--temp-dir=${workingDir}${File.separator}cache " + + "mocha $testFileAbsolutePath", + dir = workingDir, + shouldWait = true, + timeout = 20, + ) + val coveredList = mutableListOf() + val partiallyCoveredList = mutableListOf() + val uncoveredList = mutableListOf() + val db = DocumentBuilderFactory.newInstance().newDocumentBuilder() + val xmlFile = File("$coverageDataPath${File.separator}clover.xml") + val doc = db.parse(xmlFile) + buildCoverageLists( + coveredList, + partiallyCoveredList, + uncoveredList, + doc, + ) + val json = createJson( + coveredList, + partiallyCoveredList, + uncoveredList, + ) + processResult(json, outputAbsolutePath) + } + + private fun buildCoverageLists( + coveredList: MutableList, + partiallyCoveredList: MutableList, + uncoveredList: MutableList, + doc: Document, + ) { + doc.documentElement.normalize() + val lineList = try { + (((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("package").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } catch (e: Exception) { + ((doc.getElementsByTagName("project").item(0) as Element) + .getElementsByTagName("file").item(0) as Element) + .getElementsByTagName("line") + } + for (i in 0 until lineList.length) { + val lineInfo = lineList.item(i) as Element + val num = lineInfo.getAttribute("num").toInt() + val count = lineInfo.getAttribute("count").toInt() + when (lineInfo.getAttribute("type")) { + "stmt" -> { + if (count > 0) coveredList += num + else uncoveredList += num + } + + "cond" -> { + val trueCount = lineInfo.getAttribute("truecount").toInt() + val falseCount = lineInfo.getAttribute("falsecount").toInt() + when { + trueCount == 2 && falseCount == 0 -> coveredList += num + trueCount == 1 && falseCount == 1 -> partiallyCoveredList += num + trueCount == 0 && falseCount == 2 -> uncoveredList += num + } + } + } + } + } + + private fun createJson( + coveredList: List, + partiallyCoveredList: List, + uncoveredList: List, + ): JSONObject { + val coveredArray = JSONArray() + coveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + coveredArray.put(obj) + } + val partiallyCoveredArray = JSONArray() + partiallyCoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + partiallyCoveredArray.put(obj) + } + val uncoveredArray = JSONArray() + uncoveredList.forEach { + val obj = JSONObject() + obj.put("start", it) + obj.put("end", it) + uncoveredArray.put(obj) + } + val json = JSONObject() + json.put("covered", coveredArray) + json.put("notCovered", uncoveredArray) + json.put("partlyCovered", partiallyCoveredArray) + return json + } + + private fun processResult(json: JSONObject, output: String?) { + output?.let { fileName -> + val file = File(fileName) + file.createNewFile() + file.writeText(json.toString()) + } ?: logger.info { json.toString() } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt new file mode 100644 index 0000000000..05c1bb05e4 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsGenerateTestsCommand.kt @@ -0,0 +1,121 @@ +package org.utbot.cli.js + +import api.JsTestGenerator +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.flag +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.exportsLinePrefix +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.defaultTimeout +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +private val logger = KotlinLogging.logger {} + + +class JsGenerateTestsCommand : + CliktCommand(name = "generate_js", help = "Generates tests for the specified class or toplevel functions") { + + private val sourceCodeFile by option( + "-s", "--source", + help = "Specifies source code file for a generated test" + ) + .required() + .check("Must exist and ends with .js suffix") { + it.endsWith(".js") && Files.exists(Paths.get(it)) + } + + private val targetClass by option("-c", "--class", help = "Specifies target class to generate tests for") + + private val output by option("-o", "--output", help = "Specifies output file for generated tests") + .check("Must end with .js suffix") { + it.endsWith(".js") + } + + private val printToStdOut by option( + "-p", + "--print-test", + help = "Specifies whether test should be printed out to StdOut" + ) + .flag(default = false) + + private val timeout by option( + "-t", + "--timeout", + help = "Timeout for Node.js to run scripts in seconds" + ).default("$defaultTimeout") + + override fun run() { + val started = LocalDateTime.now() + try { + logger.debug { "Installing npm packages" } + logger.debug { "Generating test for [$sourceCodeFile] - started" } + val fileText = File(sourceCodeFile).readText() + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + val testGenerator = JsTestGenerator( + fileText = fileText, + sourceFilePath = makeAbsolutePath(sourceCodeFile), + parentClassName = targetClass, + outputFilePath = outputAbsolutePath, + exportsManager = ::manageExports, + timeout = timeout.toLong() + ) + val testCode = testGenerator.run() + + if (printToStdOut || (outputAbsolutePath == null && !printToStdOut)) { + logger.info { "\n$testCode" } + } + outputAbsolutePath?.let { filePath -> + val outputFile = File(filePath) + outputFile.createNewFile() + outputFile.writeText(testCode) + } + + } catch (t: Throwable) { + logger.error { "An error has occurred while generating tests for file $sourceCodeFile : $t" } + throw t + } finally { + val duration = ChronoUnit.MILLIS.between(started, LocalDateTime.now()) + logger.debug { "Generating test for [$sourceCodeFile] - completed in [$duration] (ms)" } + } + } + + private fun manageExports(exports: List) { + val exportLine = exports.joinToString(", ") + val file = File(sourceCodeFile) + val fileText = file.readText() + when { + fileText.contains("$exportsLinePrefix{$exportLine}") -> {} + fileText.contains(startComment) && !fileText.contains("$exportsLinePrefix{$exportLine}") -> { + val regex = Regex("\n$startComment\n(.*)\n$endComment") + regex.find(fileText)?.groups?.get(1)?.value?.let { + val exportsRegex = Regex("\\{(.*)}") + val existingExportsLine = exportsRegex.find(it)!!.groupValues[1] + val existingExportsSet = existingExportsLine.filterNot { c -> c == ' ' }.split(',').toMutableSet() + existingExportsSet.addAll(exports) + val resLine = existingExportsSet.joinToString() + val swappedText = fileText.replace(it, "$exportsLinePrefix{$resLine}") + file.writeText(swappedText) + } + } + + else -> { + val line = buildString { + append("\n$startComment") + append("\n$exportsLinePrefix{$exportLine}") + append("\n$endComment") + } + file.appendText(line) + } + } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt new file mode 100644 index 0000000000..043f60ea0b --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsRunTestsCommand.kt @@ -0,0 +1,60 @@ +package org.utbot.cli.js + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.check +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.js.JsUtils.makeAbsolutePath +import utils.JsCmdExec +import java.io.File + +private val logger = KotlinLogging.logger {} + +class JsRunTestsCommand : CliktCommand(name = "run_js", help = "Runs tests for the specified file or directory") { + + private val fileWithTests by option( + "--fileOrDir", "-f", + help = "Specifies a file or directory with tests" + ).required() + + private val output by option( + "-o", "--output", + help = "Specifies an output .txt file for test framework result" + ).check("Must end with .txt suffix") { + it.endsWith(".txt") + } + + private val testFramework by option("--test-framework", "-t", help = "Test framework to be used") + .choice("mocha") + .default("mocha") + + + override fun run() { + val fileWithTestsAbsolutePath = makeAbsolutePath(fileWithTests) + val dir = if (fileWithTestsAbsolutePath.endsWith(".js")) + fileWithTestsAbsolutePath.substringBeforeLast(File.separator) else fileWithTestsAbsolutePath + val outputAbsolutePath = output?.let { makeAbsolutePath(it) } + when (testFramework) { + "mocha" -> { + val (textReader, error) = JsCmdExec.runCommand( + "mocha $fileWithTestsAbsolutePath", + dir + ) + val errorText = error.readText() + if (errorText.isNotEmpty()) { + logger.error { "An error has occurred while running tests for $fileWithTests : $errorText" } + } else { + val text = textReader.readText() + outputAbsolutePath?.let { + val file = File(it) + file.createNewFile() + file.writeText(text) + } ?: logger.info { "\n$text" } + } + } + } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt new file mode 100644 index 0000000000..42b0258e21 --- /dev/null +++ b/utbot-cli-js/src/main/kotlin/org/utbot/cli/js/JsUtils.kt @@ -0,0 +1,15 @@ +package org.utbot.cli.js + +import java.io.File + +internal object JsUtils { + + @Suppress("NAME_SHADOWING") + fun makeAbsolutePath(path: String): String { + val path = path.replace("/", File.separator) + return when { + File(path).isAbsolute -> path + else -> System.getProperty("user.dir") + File.separator + path + } + } +} \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/log4j2.xml b/utbot-cli-js/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..d0f20b10bc --- /dev/null +++ b/utbot-cli-js/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-js/src/main/resources/version.properties b/utbot-cli-js/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-js/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli-python/build.gradle b/utbot-cli-python/build.gradle new file mode 100644 index 0000000000..21fc5e274b --- /dev/null +++ b/utbot-cli-python/build.gradle @@ -0,0 +1,85 @@ +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 +} + + +//noinspection GroovyAssignabilityCheck +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework-api') + implementation project(':utbot-framework') + implementation project(':utbot-summary') + implementation project(':utbot-cli') + implementation project(':utbot-python') + + implementation group: 'org.mockito', name: 'mockito-core', version: mockitoVersion + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + dependsOn project(':utbot-framework').tasks.jar + dependsOn project(':utbot-summary').tasks.jar + dependsOn project(':utbot-python').tasks.jar + dependsOn project(':utbot-fuzzers').tasks.jar + + manifest { + attributes 'Main-Class': 'org.utbot.cli.language.python.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.language.python' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Python CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt new file mode 100644 index 0000000000..82c225516a --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Application.kt @@ -0,0 +1,33 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.* +import kotlin.system.exitProcess + +class UtBotCli : CliktCommand(name = "UnitTestBot Python Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotCli().subcommands( + PythonGenerateTestsCommand(), + PythonRunTestsCommand() + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt new file mode 100644 index 0000000000..6a086f1ba4 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/Optional.kt @@ -0,0 +1,25 @@ +package org.utbot.cli.language.python + +sealed class Optional +class Fail(val message: String): Optional() +class Success(val value: A): Optional() + +fun bind( + value: Optional, + f: (A) -> Optional +): Optional = + when (value) { + is Fail -> Fail(value.message) + is Success -> f(value.value) + } + +fun pack(vararg values: Optional): Optional> { + val result = mutableListOf() + for (elem in values) { + when (elem) { + is Fail -> return Fail(elem.message) + is Success -> result.add(elem.value) + } + } + return Success(result) +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt new file mode 100644 index 0000000000..633a86ce05 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonGenerateTestsCommand.kt @@ -0,0 +1,279 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration +import org.utbot.python.code.PythonClass +import org.utbot.python.code.PythonCode +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.RequirementsUtils.installRequirements +import org.utbot.python.utils.RequirementsUtils.requirements +import org.utbot.python.utils.getModuleName +import java.io.File +import java.nio.file.Paths + +private const val DEFAULT_TIMEOUT_IN_MILLIS = 60000L +private const val DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS = 2000L + +private val logger = KotlinLogging.logger {} + +class PythonGenerateTestsCommand: CliktCommand( + name = "generate_python", + help = "Generate tests for a specified Python class or top-level functions from a specified file." +) { + private val sourceFile by argument( + help = "File with Python code to generate tests for." + ) + + private val pythonClass by option( + "-c", "--class", + help = "Specify top-level (ordinary, not nested) class under test. " + + "Without this option tests will be generated for top-level functions." + ) + + private val methods by option( + "-m", "--methods", + help = "Specify methods under test." + ).split(",") + + private val directoriesForSysPath by option( + "-s", "--sys-path", + help = "(required) Directories to add to sys.path. " + + "One of directories must contain the file with the methods under test." + ).split(",").required() + + private val pythonPath by option( + "-p", "--python-path", + help = "(required) Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", + help = "(required) File for generated tests." + ).required() + + private val coverageOutput by option( + "--coverage", + help = "File to write coverage report." + ) + + private val installRequirementsIfMissing by option( + "--install-requirements", + help = "Install Python requirements if missing." + ).flag(default = false) + + private val doNotMinimize by option( + "--do-not-minimize", + help = "Turn off minimization of the number of generated tests." + ).flag(default = false) + + private val doNotCheckRequirements by option( + "--do-not-check-requirements", + help = "Turn off Python requirements check (to speed up)." + ).flag(default = false) + + private val visitOnlySpecifiedSource by option( + "--visit-only-specified-source", + help = "Do not search for classes and imported modules in other Python files from sys.path." + ).flag(default = false) + + private val timeout by option( + "-t", "--timeout", + help = "Specify the maximum time in milliseconds to spend on generating tests ($DEFAULT_TIMEOUT_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_IN_MILLIS) + + private val timeoutForRun by option( + "--timeout-for-run", + help = "Specify the maximum time in milliseconds to spend on one function run ($DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS by default)." + ).long().default(DEFAULT_TIMEOUT_FOR_ONE_RUN_IN_MILLIS) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework to be used.") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private val testFramework: TestFramework + get() = + when (testFrameworkAsString) { + Unittest.toString() -> Unittest + Pytest.toString() -> Pytest + else -> error("Not reachable") + } + + private fun findCurrentPythonModule(): Optional { + directoriesForSysPath.forEach { path -> + val module = getModuleName(path, sourceFile) + if (module != null) + return Success(module) + } + return Fail("Couldn't find path for $sourceFile in --sys-path option. Please, specify it.") + } + + private val forbiddenMethods = listOf("__init__", "__new__") + + private fun getClassMethods(pythonClassFromSources: PythonClass): List = + pythonClassFromSources.methods.filter { method -> method.name !in forbiddenMethods } + + private fun getPythonMethods(sourceCodeContent: String, currentModule: String): Optional> { + val code = PythonCode.getFromString( + sourceCodeContent, + sourceFile.toAbsolutePath(), + pythonModule = currentModule + ) + ?: return Fail("Couldn't parse source file. Maybe it contains syntax error?") + + val topLevelFunctions = code.getToplevelFunctions() + val topLevelClasses = code.getToplevelClasses() + val selectedMethods = methods + if (pythonClass == null && methods == null) { + return if (topLevelFunctions.isNotEmpty()) + Success(topLevelFunctions) + else { + val topLevelClassMethods = topLevelClasses.flatMap { getClassMethods(it) } + if (topLevelClassMethods.isNotEmpty()) { + Success(topLevelClassMethods) + } + else + Fail("No top-level functions and top-level classes in the source file to test.") + } + } else if (pythonClass == null && selectedMethods != null) { + val pythonMethodsOpt = selectedMethods.map { functionName -> + topLevelFunctions + .find { it.name == functionName } + ?.let { Success(it) } + ?: Fail("Couldn't find top-level function $functionName in the source file.") + } + return pack(*pythonMethodsOpt.toTypedArray()) + } + + val pythonClassFromSources = code.getToplevelClasses().find { it.name == pythonClass } + ?.let { Success(it) } + ?: Fail("Couldn't find class $pythonClass in the source file.") + + val methods = bind(pythonClassFromSources) { + val fineMethods: List = it.methods.filter { method -> method.name !in forbiddenMethods } + if (fineMethods.isNotEmpty()) + Success(fineMethods) + else + Fail("No methods in definition of class $pythonClass to test.") + } + + if (selectedMethods == null) + return methods + + return bind(methods) { classFineMethods -> + pack( + *(selectedMethods.map { methodName -> + classFineMethods.find { it.name == methodName } ?.let { Success(it) } + ?: Fail("Couldn't find method $methodName of class $pythonClass") + }).toTypedArray() + ) + } + } + + private lateinit var currentPythonModule: String + private lateinit var pythonMethods: List + private lateinit var sourceFileContent: String + private lateinit var testSourceRoot: String + private lateinit var outputFilename: String + + @Suppress("UNCHECKED_CAST") + private fun calculateValues(): Optional { + val outputFile = File(output.toAbsolutePath()) + testSourceRoot = outputFile.parentFile.path + outputFilename = outputFile.name + val currentPythonModuleOpt = findCurrentPythonModule() + sourceFileContent = File(sourceFile).readText() + val pythonMethodsOpt = bind(currentPythonModuleOpt) { getPythonMethods(sourceFileContent, it) } + + return bind(pack(currentPythonModuleOpt, pythonMethodsOpt)) { + currentPythonModule = it[0] as String + pythonMethods = it[1] as List + Success(Unit) + } + } + + private fun processMissingRequirements(): PythonTestGenerationProcessor.MissingRequirementsActionResult { + if (installRequirementsIfMissing) { + logger.info("Installing requirements...") + val result = installRequirements(pythonPath) + if (result.exitValue == 0) + return PythonTestGenerationProcessor.MissingRequirementsActionResult.INSTALLED + System.err.println(result.stderr) + logger.error("Failed to install requirements.") + } else { + logger.error("Missing some requirements. Please add --install-requirements flag or install them manually.") + } + logger.info("Requirements: ${requirements.joinToString()}") + return PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED + } + + override fun run() { + val status = calculateValues() + if (status is Fail) { + logger.error(status.message) + return + } + + processTestGeneration( + pythonPath = pythonPath, + testSourceRoot = testSourceRoot, + pythonFilePath = sourceFile.toAbsolutePath(), + pythonFileContent = sourceFileContent, + directoriesForSysPath = directoriesForSysPath.map { it.toAbsolutePath() } .toSet(), + currentPythonModule = currentPythonModule, + pythonMethods = pythonMethods, + containingClassName = pythonClass, + timeout = timeout, + testFramework = testFramework, + timeoutForRun = timeoutForRun, + withMinimization = !doNotMinimize, + doNotCheckRequirements = doNotCheckRequirements, + visitOnlySpecifiedSource = visitOnlySpecifiedSource, + writeTestTextToFile = { generatedCode -> + val file = File(output) + file.parentFile?.mkdirs() + file.writeText(generatedCode) + file.createNewFile() + }, + checkingRequirementsAction = { + logger.info("Checking requirements...") + }, + requirementsAreNotInstalledAction = ::processMissingRequirements, + startedLoadingPythonTypesAction = { + logger.info("Loading information about Python types...") + }, + startedTestGenerationAction = { + logger.info("Generating tests...") + }, + notGeneratedTestsAction = { + logger.error( + "Couldn't generate tests for the following functions: ${it.joinToString()}" + ) + }, + processMypyWarnings = { messages -> messages.forEach { println(it) } }, + finishedAction = { + logger.info("Finished test generation for the following functions: ${it.joinToString()}") + }, + processCoverageInfo = { coverageReport -> + val output = coverageOutput ?: return@processTestGeneration + val file = File(output) + file.writeText(coverageReport) + file.parentFile?.mkdirs() + file.createNewFile() + }, + pythonRunRoot = Paths.get("").toAbsolutePath() + ) + } + + private fun String.toAbsolutePath(): String = + File(this).canonicalPath +} diff --git a/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt new file mode 100644 index 0000000000..5ce3c43840 --- /dev/null +++ b/utbot-cli-python/src/main/kotlin/org/utbot/cli/language/python/PythonRunTestsCommand.kt @@ -0,0 +1,84 @@ +package org.utbot.cli.language.python + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.arguments.argument +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.required +import com.github.ajalt.clikt.parameters.types.choice +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.utils.CmdResult +import org.utbot.python.utils.runCommand +import java.io.File +import java.nio.file.Paths +import kotlin.system.exitProcess + +class PythonRunTestsCommand : CliktCommand(name = "run_python", help = "Run tests in the specified file") { + + private val sourceFile by argument( + help = "File with Python tests to run." + ) + + private val pythonPath by option( + "-p", "--python-path", + help = "Path to Python interpreter." + ).required() + + private val output by option( + "-o", "--output", + help = "Specify file for report." + ) + + private val testFrameworkAsString by option("--test-framework", help = "Test framework of tests to run") + .choice(Pytest.toString(), Unittest.toString()) + .default(Unittest.toString()) + + private fun runUnittest(): CmdResult { + val currentPath = Paths.get("").toAbsolutePath().toString() + val sourceFilePath = Paths.get(sourceFile).toAbsolutePath().toString() + return if (sourceFilePath.startsWith(currentPath)) { + runCommand( + listOf( + pythonPath, + "-m", + "unittest", + sourceFile + ) + ) + } + else CmdResult( + "", + "File $sourceFile can not be imported from Unittest. Move test file to child directory or use pytest.", + 1 + ) + } + + private fun runPytest(): CmdResult = + runCommand(listOf( + pythonPath, + "-m", + "pytest", + sourceFile + )) + + override fun run() { + val result = + when (testFrameworkAsString) { + Unittest.toString() -> runUnittest() + Pytest.toString() -> runPytest() + else -> error("Not reachable") + } + + output?.let { + val file = File(it) + file.writeText(result.stderr + result.stdout) + file.parentFile?.mkdirs() + file.createNewFile() + } + System.err.println(result.stderr) + println(result.stdout) + + exitProcess(result.exitValue) + } +} \ No newline at end of file diff --git a/utbot-cli-python/src/main/resources/log4j2.xml b/utbot-cli-python/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-python/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-python/src/main/resources/version.properties b/utbot-cli-python/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-python/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-cli/src/main/kotlin/org/utbot/cli/Application.kt b/utbot-cli/src/main/kotlin/org/utbot/cli/Application.kt index b8d1f5a683..ceef9b93e4 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/Application.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/Application.kt @@ -28,7 +28,11 @@ class UtBotCli : CliktCommand(name = "UnitTestBot Java Command Line Interface") } fun main(args: Array) = try { - UtBotCli().subcommands(GenerateTestsCommand(), BunchTestGeneratorCommand(), RunTestsCommand()).main(args) + UtBotCli().subcommands( + GenerateTestsCommand(), + BunchTestGeneratorCommand(), + RunTestsCommand(), + ).main(args) } catch (ex: Throwable) { ex.printStackTrace() exitProcess(1) diff --git a/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt index 4bb386ae7b..e13170ddf0 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt @@ -13,6 +13,7 @@ import java.nio.file.StandardCopyOption import java.nio.file.attribute.BasicFileAttributes import java.time.Duration import java.util.concurrent.TimeUnit +import java.util.zip.ZipEntry import java.util.zip.ZipFile import kotlin.concurrent.thread import kotlin.streams.asSequence @@ -30,12 +31,14 @@ object FileUtil { fun extractArchive( archiveFile: Path, destPath: Path, - vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING) + vararg options: CopyOption = arrayOf(StandardCopyOption.REPLACE_EXISTING), + extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true } ) { Files.createDirectories(destPath) ZipFile(archiveFile.toFile()).use { archive -> val entries = archive.stream().asSequence() + .filter(extractOnlySuchEntriesPredicate) .sortedBy { it.name } .toList() @@ -175,12 +178,26 @@ object FileUtil { /** * Extracts archive to temp directory and returns path to directory. */ - fun extractArchive(archiveFile: Path): Path { + fun extractArchive(archiveFile: Path, extractOnlySuchEntriesPredicate: (ZipEntry) -> Boolean = { true }): Path { val tempDir = createTempDirectory(TEMP_DIR_NAME).toFile().apply { deleteOnExit() } - extractArchive(archiveFile, tempDir.toPath()) + extractArchive(archiveFile, tempDir.toPath(), extractOnlySuchEntriesPredicate = extractOnlySuchEntriesPredicate) return tempDir.toPath() } + /** + * Extracts specified directory (with all contents) from archive to temp directory and returns path to it. + */ + fun extractDirectoryFromArchive(archiveFile: Path, directoryName: String): Path? { + val extractedJarDirectory = extractArchive(archiveFile) { entry -> + entry.name.normalizePath().startsWith(directoryName) + } + val extractedTargetDirectoryPath = extractedJarDirectory.resolve(directoryName) + if(!extractedTargetDirectoryPath.toFile().exists()) { + return null + } + return extractedTargetDirectoryPath + } + /** * Returns the path to the class files for the given ClassLocation. */ diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt index b50e0ff4fd..26245ce050 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/Api.kt @@ -236,7 +236,7 @@ data class UtError( * * UtNullModel represents nulls, other models represent not-nullable entities. */ -sealed class UtModel( +open class UtModel( open val classId: ClassId ) @@ -675,6 +675,7 @@ data class UtStaticMethodInstrumentation( val values: List ) : UtInstrumentation() + val SootClass.id: ClassId get() = ClassId(name) @@ -726,7 +727,7 @@ val Type.classId: ClassId */ open class ClassId @JvmOverloads constructor( val name: String, - val elementClassId: ClassId? = null, + open val elementClassId: ClassId? = null, // Treat simple class ids as non-nullable open val isNullable: Boolean = false ) { @@ -1247,7 +1248,9 @@ enum class CodegenLanguage( @Suppress("unused") override val description: String = "Generate unit tests in $displayName" ) : CodeGenerationSettingItem { JAVA(id = "Java", displayName = "Java"), - KOTLIN(id = "Kotlin", displayName = "Kotlin (experimental)"); + KOTLIN(id = "Kotlin", displayName = "Kotlin (experimental)"), + JS(id = "JavaScript", displayName = "JavaScript"), + PYTHON(id = "Python", displayName = "Python"); enum class OperatingSystem { WINDOWS, @@ -1272,18 +1275,21 @@ enum class CodegenLanguage( get() = when (this) { JAVA -> listOf(System.getenv("JAVA_HOME"), "bin", "javac") KOTLIN -> listOf(System.getenv("KOTLIN_HOME"), "bin", kotlinCompiler) + else -> throw UnsupportedOperationException() }.joinToString(File.separator) val extension: String get() = when (this) { JAVA -> ".java" KOTLIN -> ".kt" + else -> throw UnsupportedOperationException() } val executorInvokeCommand: String get() = when (this) { JAVA -> listOf(System.getenv("JAVA_HOME"), "bin", "java") KOTLIN -> listOf(System.getenv("JAVA_HOME"), "bin", "java") + else -> throw UnsupportedOperationException() }.joinToString(File.separator) override fun toString(): String = id @@ -1297,6 +1303,7 @@ enum class CodegenLanguage( ).plus(sourcesFiles) KOTLIN -> listOf("-d", buildDirectory, "-jvm-target", jvmTarget, "-cp", classPath).plus(sourcesFiles) + else -> throw UnsupportedOperationException() } if (this == KOTLIN && System.getenv("KOTLIN_HOME") == null) { throw RuntimeException("'KOTLIN_HOME' environment variable is not defined. Standard location is {IDEA installation dir}/plugins/Kotlin/kotlinc") @@ -1410,3 +1417,4 @@ class DocRegularStmt(val stmt: String) : DocStatement() { override fun hashCode(): Int = stmt.hashCode() } + diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt index 6ab14e5aac..522a4b331a 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/CoverageApi.kt @@ -26,7 +26,7 @@ data class Instruction( * @param instructionsCount a number of all instructions in the current class. * */ -data class Coverage( +open class Coverage( val coveredInstructions: List = emptyList(), val instructionsCount: Long? = null ) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/go/GoApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/go/GoApi.kt new file mode 100644 index 0000000000..592a0e0523 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/go/GoApi.kt @@ -0,0 +1,75 @@ +package org.utbot.framework.plugin.api.go + +import org.utbot.framework.plugin.api.* + +/** + * Parent class for all Go types for compatibility with UTBot framework. + * + * To see its children check GoTypesApi.kt at org.utbot.go.api. + */ +open class GoClassId(private val goName: String) : ClassId(goName) { + + override fun toString(): String = goName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is GoClassId) return false + + return name == other.name + } + + override fun hashCode(): Int = name.hashCode() + + override val isNullable: Boolean + get() = error("not supported") + override val canonicalName: String + get() = error("not supported") + override val simpleName: String + get() = error("not supported") + override val packageName: String + get() = error("not supported") + override val isInDefaultPackage: Boolean + get() = error("not supported") + override val isPublic: Boolean + get() = error("not supported") + override val isProtected: Boolean + get() = error("not supported") + override val isPrivate: Boolean + get() = error("not supported") + override val isFinal: Boolean + get() = error("not supported") + override val isStatic: Boolean + get() = error("not supported") + override val isAbstract: Boolean + get() = error("not supported") + override val isAnonymous: Boolean + get() = error("not supported") + override val isLocal: Boolean + get() = error("not supported") + override val isInner: Boolean + get() = error("not supported") + override val isNested: Boolean + get() = error("not supported") + override val isSynthetic: Boolean + get() = error("not supported") + override val allMethods: Sequence + get() = error("not supported") + override val allConstructors: Sequence + get() = error("not supported") + override val typeParameters: TypeParameters + get() = error("not supported") + override val outerClass: Class<*>? + get() = error("not supported") + override val simpleNameWithEnclosings: String + get() = error("not supported") +} + +/** + * Parent class for all Go models. + * + * To see its children check GoUtModelsApi.kt at org.utbot.go.api. + */ +open class GoUtModel( + override val classId: GoClassId, + val requiredImports: Set +) : UtModel(classId) \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt new file mode 100644 index 0000000000..7e7b944c90 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt @@ -0,0 +1,160 @@ +package org.utbot.framework.plugin.api.js + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.js.util.toJsClassId +import org.utbot.framework.plugin.api.primitiveModelValueToClassId + +open class JsClassId( + private val jsName: String, + private val methods: Sequence = emptySequence(), + private val constructor: JsConstructorId? = null, + private val classPackagePath: String = "", + private val classFilePath: String = "", + override val elementClassId: JsClassId? = null +) : ClassId(jsName, elementClassId) { + override val simpleName: String + get() = jsName + + override val allMethods: Sequence + get() = methods + + override val allConstructors: Sequence + get() = if (constructor == null) emptySequence() else sequenceOf(constructor) + + override val packageName: String + get() = classPackagePath + + override val canonicalName: String + get() = jsName + + //TODO SEVERE: Check if overrides are correct + override val isAbstract: Boolean + get() = false + + override val isAnonymous: Boolean + get() = false + + override val isFinal: Boolean + get() = false + + override val isInDefaultPackage: Boolean + get() = false + + override val isInner: Boolean + get() = false + + override val isLocal: Boolean + get() = false + + override val isNested: Boolean + get() = false + + override val isNullable: Boolean + get() = false + + override val isPrivate: Boolean + get() = false + + override val isProtected: Boolean + get() = false + + override val isPublic: Boolean + get() = true + + //TODO SEVERE: isStatic is definitely incorrect! + override val isStatic: Boolean + get() = false + + override val isSynthetic: Boolean + get() = false + + override val outerClass: Class<*>? + get() = null + + val filePath: String + get() = classFilePath + +} + +class JsEmptyClassId : JsClassId("empty") +class JsMethodId( + override var classId: JsClassId, + override val name: String, + private val returnTypeNotLazy: JsClassId, + private val parametersNotLazy: List, + private val staticModifier: Boolean = false, + private val lazyReturnType: Lazy? = null, + private val lazyParameters: Lazy>? = null +) : MethodId(classId, name, returnTypeNotLazy, parametersNotLazy) { + + override val parameters: List + get() = lazyParameters?.value ?: parametersNotLazy + + override val returnType: JsClassId + get() = lazyReturnType?.value ?: returnTypeNotLazy + + override val isPrivate: Boolean + get() = throw UnsupportedOperationException("JavaScript does not support private methods.") + + override val isProtected: Boolean + get() = throw UnsupportedOperationException("JavaScript does not support protected methods.") + + override val isPublic: Boolean + get() = true + + override val isStatic: Boolean + get() = staticModifier + +} + +class JsConstructorId( + override var classId: JsClassId, + override val parameters: List, +) : ConstructorId(classId, parameters) { + + override val returnType: JsClassId + get() = classId + + override val isPrivate: Boolean + get() = throw UnsupportedOperationException("JavaScript does not support private constructors.") + + override val isProtected: Boolean + get() = throw UnsupportedOperationException("JavaScript does not support protected constructors.") + + override val isPublic: Boolean + get() = true +} + +class JsMultipleClassId(private val jsJoinedName: String) : JsClassId(jsJoinedName) { + + val types: Sequence + get() = jsJoinedName.split('|').map { JsClassId(it) }.asSequence() +} + +open class JsUtModel( + override val classId: JsClassId +) : UtModel(classId) + +class JsNullModel( + override val classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "null" +} + +class JsUndefinedModel( + classId: JsClassId +) : JsUtModel(classId) { + override fun toString() = "undefined" +} + +data class JsPrimitiveModel( + val value: Any, +) : JsUtModel(jsPrimitiveModelValueToClassId(value)) { + override fun toString() = value.toString() +} + +private fun jsPrimitiveModelValueToClassId(value: Any) = + primitiveModelValueToClassId(value).toJsClassId() \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/util/JsIdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/util/JsIdUtil.kt new file mode 100644 index 0000000000..488cec0478 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/util/JsIdUtil.kt @@ -0,0 +1,55 @@ +package org.utbot.framework.plugin.api.js.util + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.doubleClassId +import org.utbot.framework.plugin.api.util.floatClassId +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.shortClassId + +val jsUndefinedClassId = JsClassId("undefined") +val jsNumberClassId = JsClassId("number") +val jsBooleanClassId = JsClassId("bool") +val jsDoubleClassId = JsClassId("double") +val jsStringClassId = JsClassId("string") +val jsErrorClassId = JsClassId("error") + + +val jsPrimitives = setOf( + jsNumberClassId, + jsBooleanClassId, + jsDoubleClassId, +) + +val jsBasic = setOf( + jsNumberClassId, + jsBooleanClassId, + jsDoubleClassId, + jsUndefinedClassId, + jsStringClassId, +) + +fun ClassId.toJsClassId() = + when { + this == intClassId -> jsNumberClassId + this == byteClassId -> jsNumberClassId + this == shortClassId -> jsNumberClassId + this == booleanClassId -> jsBooleanClassId + this == doubleClassId -> jsDoubleClassId + this == floatClassId -> jsDoubleClassId + this.name.lowercase().contains("string") -> jsStringClassId + this == longClassId -> jsNumberClassId + else -> jsUndefinedClassId + } + +val JsClassId.isJsBasic: Boolean + get() = this in jsBasic + +val JsClassId.isJsPrimitive: Boolean + get() = this in jsPrimitives + +val JsClassId.isUndefined: Boolean + get() = this == jsUndefinedClassId \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonApi.kt new file mode 100644 index 0000000000..98c8bfd24c --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonApi.kt @@ -0,0 +1,179 @@ +package org.utbot.framework.plugin.api.python + +import org.utbot.common.withToStringThreadLocalReentrancyGuard +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.python.util.moduleOfType + +/** + * PythonClassId represents Python type. + * NormalizedPythonAnnotation represents annotation after normalization. + * + * Example of PythonClassId, but not NormalizedPythonAnnotation: + * builtins.list (normalized annotation is typing.List[typing.Any]) + */ + +const val pythonBuiltinsModuleName = "builtins" + +class PythonClassId( + name: String // includes module (like "_ast.Assign") +) : ClassId(name) { + override fun toString(): String = name + val rootModuleName: String = this.toString().split(".")[0] + override val simpleName: String = name.split(".").last() + val moduleName: String + get() { + return moduleOfType(name) ?: pythonBuiltinsModuleName + } + override val packageName = moduleName + override val canonicalName = name +} + +open class RawPythonAnnotation( + annotation: String +): ClassId(annotation) + +class NormalizedPythonAnnotation( + annotation: String +) : RawPythonAnnotation(annotation) + +class PythonMethodId( + override val classId: PythonClassId, // may be a fake class for top-level functions + override val name: String, + override val returnType: RawPythonAnnotation, + override val parameters: List, +) : MethodId(classId, name, returnType, parameters) { + val moduleName: String = classId.moduleName + val rootModuleName: String = this.toString().split(".")[0] + override fun toString(): String = if (moduleName.isNotEmpty()) "$moduleName.$name" else name +} + +sealed class PythonModel(classId: PythonClassId): UtModel(classId) { + open val allContainingClassIds: Set = setOf(classId) +} + +class PythonTreeModel( + val tree: PythonTree.PythonTreeNode, + classId: PythonClassId, +): PythonModel(classId) { + override val allContainingClassIds: Set + get() { + val children = tree.children.map { PythonTreeModel(it, it.type) } + return super.allContainingClassIds + children.flatMap { it.allContainingClassIds } + } +} + +class PythonDefaultModel( + val repr: String, + classId: PythonClassId +): PythonModel(classId) { + override fun toString() = repr +} + +// none annotation can be used in code only since Python 3.10 +val pythonNoneClassId = PythonClassId("types.NoneType") +val pythonAnyClassId = NormalizedPythonAnnotation("typing.Any") +val pythonIntClassId = PythonClassId("builtins.int") +val pythonFloatClassId = PythonClassId("builtins.float") +val pythonStrClassId = PythonClassId("builtins.str") +val pythonBoolClassId = PythonBoolModel.classId +val pythonRangeClassId = PythonClassId("builtins.range") +val pythonListClassId = PythonListModel.classId +val pythonTupleClassId = PythonTupleModel.classId +val pythonDictClassId = PythonDictModel.classId +val pythonSetClassId = PythonSetModel.classId + +class PythonPrimitiveModel( + val value: Any, + classId: PythonClassId +): PythonModel(classId) { + override fun toString() = "$value" +} + +class PythonBoolModel(val value: Boolean): PythonModel(classId) { + override fun toString() = + if (value) "True" else "False" + companion object { + val classId = PythonClassId("builtins.bool") + } +} + +class PythonInitObjectModel( + val type: String, + val initValues: List +): PythonModel(PythonClassId(type)) { + override fun toString(): String { + val params = initValues.joinToString(separator = ", ") { it.toString() } + return "$type($params)" + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + initValues.flatMap { it.allContainingClassIds } +} + +class PythonListModel( + val length: Int = 0, + val stores: List +) : PythonModel(classId) { + override fun toString() = + (0 until length).joinToString(", ", "[", "]") { stores[it].toString() } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.list") + } +} + +class PythonTupleModel( + val length: Int = 0, + val stores: List +) : PythonModel(classId) { + override fun toString() = + (0 until length).joinToString(", ", "(", ")") { stores[it].toString() } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.tuple") + } +} + +class PythonDictModel( + val length: Int = 0, + val stores: Map +) : PythonModel(classId) { + override fun toString() = withToStringThreadLocalReentrancyGuard { + stores.entries.joinToString(", ", "{", "}") { "${it.key}: ${it.value}" } + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + + stores.entries.flatMap { it.key.allContainingClassIds + it.value.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.dict") + } +} + +class PythonSetModel( + val length: Int = 0, + val stores: Set +) : PythonModel(classId) { + override fun toString() = withToStringThreadLocalReentrancyGuard { + if (stores.isEmpty()) + "set()" + else + stores.joinToString(", ", "{", "}") { it.toString() } + } + + override val allContainingClassIds: Set + get() = super.allContainingClassIds + stores.flatMap { it.allContainingClassIds } + + companion object { + val classId = PythonClassId("builtins.set") + } +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonTree.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonTree.kt new file mode 100644 index 0000000000..b1a3d1044c --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/PythonTree.kt @@ -0,0 +1,134 @@ +package org.utbot.framework.plugin.api.python + +object PythonTree { + open class PythonTreeNode( + val type: PythonClassId, + var comparable: Boolean = true + ) { + open val children: List = emptyList() + + open fun typeEquals(other: Any?): Boolean { + return if (other is PythonTreeNode) + type == other.type && comparable && other.comparable + else + false + } + } + + class PrimitiveNode( + type: PythonClassId, + val repr: String, + ): PythonTreeNode(type) + + class ListNode( + val items: List + ): PythonTreeNode(PythonClassId("builtins.list")) { + override val children: List + get() = items + + override fun typeEquals(other: Any?): Boolean { + return if (other is ListNode) + items.zip(other.items).all { + it.first.typeEquals(it.second) + } + else false + } + } + + class DictNode( + val items: Map + ): PythonTreeNode(PythonClassId("builtins.dict")) { + override val children: List + get() = items.values + items.keys + + override fun typeEquals(other: Any?): Boolean { + return if (other is DictNode) { + items.keys.size == other.items.keys.size && items.keys.all { + items[it]?.typeEquals(other.items[it]) ?: false + } + + } else false + } + } + + class SetNode( + val items: Set + ): PythonTreeNode(PythonClassId("builtins.set")) { + override val children: List + get() = items.toList() + + override fun typeEquals(other: Any?): Boolean { + return if (other is SetNode) { + items.size == other.items.size && ( + items.isEmpty() || items.all { + items.first().typeEquals(it) + } && other.items.all { + items.first().typeEquals(it) + }) + } else { + false + } + } + } + + class TupleNode( + val items: List + ): PythonTreeNode(PythonClassId("builtins.tuple")) { + override val children: List + get() = items + + override fun typeEquals(other: Any?): Boolean { + return if (other is TupleNode) { + items.size == other.items.size && items.zip(other.items).all { + it.first.typeEquals(it.second) + } + } else { + false + } + } + } + + class ReduceNode( + val id: Long, + type: PythonClassId, + val constructor: PythonClassId, + val args: List, + var state: Map, + var listitems: List, + var dictitems: Map, + ): PythonTreeNode(type) { + constructor( + id: Long, + type: PythonClassId, + constructor: PythonClassId, + args: List, + ): this(id, type, constructor, args, emptyMap(), emptyList(), emptyMap()) + + override val children: List + get() = args + state.values + listitems + dictitems.values + dictitems.keys + PythonTreeNode(constructor) + + override fun typeEquals(other: Any?): Boolean { + return if (other is ReduceNode) { + type == other.type && state.all { (key, value) -> + other.state.containsKey(key) && value.typeEquals(other.state[key]) + } && listitems.withIndex().all { (index, item) -> + other.listitems.size > index && item.typeEquals(other.listitems[index]) + } && dictitems.all { (key, value) -> + other.dictitems.containsKey(key) && value.typeEquals(other.dictitems[key]) + } + } + else false + } + } + + fun allElementsHaveSameStructure(elements: Collection): Boolean { + return if (elements.isEmpty()) { + true + } else { + val firstElement = elements.first() + elements.drop(1).all { + it.typeEquals(firstElement) + } + } + } +} \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonIdUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonIdUtils.kt new file mode 100644 index 0000000000..3b65b7b754 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonIdUtils.kt @@ -0,0 +1,16 @@ +package org.utbot.framework.plugin.api.python.util + +import org.utbot.framework.plugin.api.python.* + +// none annotation can be used in code only since Python 3.10 +val pythonNoneClassId = PythonClassId("types.NoneType") +val pythonAnyClassId = NormalizedPythonAnnotation("typing.Any") +val pythonIntClassId = PythonClassId("builtins.int") +val pythonFloatClassId = PythonClassId("builtins.float") +val pythonStrClassId = PythonClassId("builtins.str") +val pythonBoolClassId = PythonBoolModel.classId +val pythonRangeClassId = PythonClassId("builtins.range") +val pythonListClassId = PythonListModel.classId +val pythonTupleClassId = PythonTupleModel.classId +val pythonDictClassId = PythonDictModel.classId +val pythonSetClassId = PythonSetModel.classId \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonUtils.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonUtils.kt new file mode 100644 index 0000000000..f4881e7779 --- /dev/null +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/python/util/PythonUtils.kt @@ -0,0 +1,16 @@ +package org.utbot.framework.plugin.api.python.util + + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun String.toSnakeCase(): String { + val splitSymbols = "_" + return this.mapIndexed { index: Int, c: Char -> + if (c.isLowerCase() || c.isDigit() || splitSymbols.contains(c)) c + else if (c.isUpperCase()) { (if (index > 0) "_" else "") + c.lowercase() } + else c + }.joinToString("") +} diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt index 62e077cfdc..ff7f8f097f 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/util/IdUtil.kt @@ -1,5 +1,6 @@ package org.utbot.framework.plugin.api.util +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.BuiltinConstructorId import org.utbot.framework.plugin.api.BuiltinMethodId @@ -346,7 +347,10 @@ val ClassId.isPrimitive: Boolean get() = this in primitives val ClassId.isPrimitiveArray: Boolean - get() = elementClassId != null && elementClassId.isPrimitive + get() { + val curElementClassId = elementClassId + return curElementClassId != null && curElementClassId.isPrimitive + } val ClassId.isPrimitiveWrapper: Boolean get() = this in primitiveWrappers diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt index ff436d10ff..b85a5ebcf3 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt @@ -193,6 +193,8 @@ class ValueConstructor { is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model)) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + // Python, JavaScript and go are supposed to be here as well + else -> UtConcreteValue(null, model.classId.jClass) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt index bd454e3e83..90c95df215 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/assemble/AssembleModelGenerator.kt @@ -189,6 +189,8 @@ class AssembleModelGenerator(private val basePackageName: String) { is UtArrayModel -> assembleArrayModel(utModel) is UtCompositeModel -> assembleCompositeModel(utModel) is UtAssembleModel -> assembleAssembleModel(utModel) + // Python, JavaScript and go are supposed to be here as well + else -> utModel } } catch (e: AssembleException) { utModel diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt index b57d9e0cd9..466dcff038 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Domain.kt @@ -5,6 +5,8 @@ import org.utbot.framework.codegen.model.constructor.builtin.mockitoClassId import org.utbot.framework.codegen.model.constructor.builtin.ongoingStubbingClassId import org.utbot.framework.codegen.model.constructor.util.argumentsClassId import org.utbot.framework.codegen.model.tree.CgClassId +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.util.* import org.utbot.framework.plugin.api.BuiltinClassId import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodeGenerationSettingBox @@ -35,7 +37,7 @@ import org.utbot.framework.plugin.api.util.voidWrapperClassId data class TestClassFile(val packageName: String, val imports: List, val testClass: String) -sealed class Import(internal val order: Int) : Comparable { +abstract class Import(val order: Int) : Comparable { abstract val qualifiedName: String override fun compareTo(other: Import) = importComparator.compare(this, other) @@ -101,7 +103,7 @@ fun testFrameworkByName(testFramework: String): TestFramework = * This feature allows to enable additional mockito-core settings required for static mocking. * It is implemented via adding special file "MockMaker" into test project resources. */ -sealed class StaticsMocking( +abstract class StaticsMocking( var isConfigured: Boolean = false, override val id: String, override val displayName: String, @@ -119,6 +121,7 @@ sealed class StaticsMocking( } } + object NoStaticMocking : StaticsMocking( id = "No static mocking", displayName = "No static mocking", @@ -171,7 +174,7 @@ object MockitoStaticMocking : StaticsMocking(id = "Mockito static mocking", disp ) } -sealed class TestFramework( +abstract class TestFramework( override val id: String, override val displayName: String, override val description: String = "Use $displayName as test framework", @@ -193,7 +196,9 @@ sealed class TestFramework( abstract val nestedClassesShouldBeStatic: Boolean abstract val argListClassId: ClassId - val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } + open val testSuperClass: ClassId? = null + + open val assertEquals by lazy { assertionId("assertEquals", objectClassId, objectClassId) } val assertFloatEquals by lazy { assertionId("assertEquals", floatClassId, floatClassId, floatClassId) } @@ -227,10 +232,10 @@ sealed class TestFramework( val assertNotEquals by lazy { assertionId("assertNotEquals", objectClassId, objectClassId) } - protected fun assertionId(name: String, vararg params: ClassId): MethodId = + protected open fun assertionId(name: String, vararg params: ClassId): MethodId = builtinStaticMethodId(assertionsClass, name, voidClassId, *params) private fun arrayAssertionId(name: String, vararg params: ClassId): MethodId = - builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) + builtinStaticMethodId(arraysAssertionsClass, name, voidClassId, *params) abstract fun getRunTestsCommand( executionInvoke: String, @@ -593,6 +598,7 @@ object Junit5 : TestFramework(id = "JUnit5", displayName = "JUnit 5") { } } + enum class RuntimeExceptionTestsBehaviour( override val id: String, override val displayName: String, @@ -617,6 +623,7 @@ enum class RuntimeExceptionTestsBehaviour( companion object : CodeGenerationSettingBox { override val defaultItem: RuntimeExceptionTestsBehaviour get() = FAIL override val allItems: List = values().toList() +// val pythonItems: List get() = listOf(Unittest, Pytest) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt index b4f248ddef..c85c61ab96 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt @@ -1,5 +1,6 @@ package org.utbot.framework.codegen +import org.utbot.framework.plugin.api.CodeGenLanguage import org.utbot.framework.plugin.api.CodegenLanguage private val javaKeywords = setOf( @@ -32,10 +33,5 @@ private val kotlinModifierKeywords = setOf( // For now we check only hard keywords because others can be used as methods and variables identifiers private val kotlinKeywords = kotlinHardKeywords -private fun getLanguageKeywords(codegenLanguage: CodegenLanguage): Set = when(codegenLanguage) { - CodegenLanguage.JAVA -> javaKeywords - CodegenLanguage.KOTLIN -> kotlinKeywords -} - -fun isLanguageKeyword(word: String, codegenLanguage: CodegenLanguage): Boolean = - word in getLanguageKeywords(codegenLanguage) +fun isLanguageKeyword(word: String, codegenLanguage: CodeGenLanguage): Boolean = + word in codegenLanguage.languageKeywords diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt index a8e632976c..851da3e686 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.model +import org.utbot.framework.codegen.* import org.utbot.framework.codegen.ForceStaticMocking import org.utbot.framework.codegen.HangingTestsTimeout import org.utbot.framework.codegen.ParametrizedTestSource @@ -17,17 +18,13 @@ import org.utbot.framework.codegen.model.constructor.tree.TestsGenerationReport import org.utbot.framework.codegen.model.tree.AbstractCgClassFile import org.utbot.framework.codegen.model.tree.CgRegularClassFile import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.plugin.api.* import org.utbot.framework.codegen.model.tree.CgComment import org.utbot.framework.codegen.model.tree.CgSingleLineComment -class CodeGenerator( - private val classUnderTest: ClassId, +open class CodeGenerator( + val classUnderTest: ClassId, paramNames: MutableMap> = mutableMapOf(), generateUtilClassFile: Boolean = false, testFramework: TestFramework = TestFramework.defaultItem, @@ -42,13 +39,14 @@ class CodeGenerator( enableTestsTimeout: Boolean = true, testClassPackageName: String = classUnderTest.packageName, ) { - private var context: CgContext = CgContext( + open var context: CgContext = CgContext( classUnderTest = classUnderTest, generateUtilClassFile = generateUtilClassFile, paramNames = paramNames, testFramework = testFramework, mockFramework = mockFramework, codegenLanguage = codegenLanguage, + codeGenLanguage = if (codegenLanguage == CodegenLanguage.JAVA) JavaCodeLanguage else KotlinCodeLanguage, parametrizedTestSource = parameterizedTestSource, staticsMocking = staticsMocking, forceStaticMocking = forceStaticMocking, @@ -92,7 +90,7 @@ class CodeGenerator( * - turns on imports optimization in code generator * - passes a custom test class name if there is one */ - private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { val prevContext = context return try { context = prevContext.copy( @@ -105,7 +103,7 @@ class CodeGenerator( } } - private fun renderClassFile(file: AbstractCgClassFile<*>): String { + fun renderClassFile(file: AbstractCgClassFile<*>): String { val renderer = CgAbstractRenderer.makeRenderer(context) file.accept(renderer) return renderer.toString() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt index 80994c49b4..989201c040 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.UtFuzzedExecution import soot.jimple.JimpleBody -data class CgMethodTestSet private constructor( +data class CgMethodTestSet constructor( val executableId: ExecutableId, val jimpleBody: JimpleBody? = null, val errors: Map = emptyMap(), @@ -31,6 +31,35 @@ data class CgMethodTestSet private constructor( ) { executions = from.executions } + /** + * For JavaScript purposes. + * todo: consider to remove + */ + constructor( + executableId: ExecutableId, + execs: List = emptyList(), + errors: Map = emptyMap() + ) : this( + executableId, + null, + errors, + listOf(null to execs.indices) + ) { + executions = execs + } + + constructor( + executableId: ExecutableId, + executions: List = emptyList(), + ) : this( + executableId, + null, + emptyMap(), + + listOf(null to executions.indices) + ) { + this.executions = executions + } /** * Splits [CgMethodTestSet] into separate test sets having diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt index 4d6f764483..958c60c63e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/TestClassContext.kt @@ -10,7 +10,7 @@ import org.utbot.framework.codegen.model.tree.CgTestClass * This class stores context information needed to build [CgTestClass]. * Should only be used in [CgContextOwner]. */ -internal data class TestClassContext( +data class TestClassContext( // set of interfaces that the test class must inherit val collectedTestClassInterfaces: MutableSet = mutableSetOf(), diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt index 87ee5c48f5..4d5611e3eb 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/UtilMethodBuiltins.kt @@ -30,7 +30,7 @@ import java.lang.reflect.Method * The class may actually not have some of these methods if they * are not required in the process of code generation (this is the case for [TestClassUtilMethodProvider]). */ -internal abstract class UtilMethodProvider(val utilClassId: ClassId) { +abstract class UtilMethodProvider(val utilClassId: ClassId) { val utilMethodIds: Set get() = setOf( getUnsafeInstanceMethodId, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt index 308bab95bc..1f2b7d1870 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt @@ -32,16 +32,7 @@ import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider import org.utbot.framework.codegen.model.constructor.TestClassContext import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.tree.CgParameterKind -import org.utbot.framework.plugin.api.BuiltinClassId -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.ExecutableId -import org.utbot.framework.plugin.api.FieldId -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.UtExecution -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.isCheckedException import org.utbot.framework.plugin.api.util.isSubtypeOf @@ -62,7 +53,7 @@ import org.utbot.framework.plugin.api.util.jClass * * @see [CgContextOwner.withNameScope] */ -internal interface CgContextOwner { +interface CgContextOwner { // current class under test val classUnderTest: ClassId @@ -424,7 +415,7 @@ internal interface CgContextOwner { /** * Context with current code generation info */ -internal data class CgContext( +data class CgContext( override val classUnderTest: ClassId, val generateUtilClassFile: Boolean, override var currentExecutable: ExecutableId? = null, @@ -445,6 +436,7 @@ internal data class CgContext( override val forceStaticMocking: ForceStaticMocking, override val generateWarningsForStaticMocking: Boolean, override val codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + open val codeGenLanguage: CodeGenLanguage = CodeGenLanguage.defaultItem, override val parametrizedTestSource: ParametrizedTestSource = ParametrizedTestSource.DO_NOT_PARAMETRIZE, override var mockFrameworkUsed: Boolean = false, override var currentBlock: PersistentList = persistentListOf(), @@ -475,7 +467,7 @@ internal data class CgContext( override val outerMostTestClassContext: TestClassContext get() = _outerMostTestClassContext ?: error("Accessing outerMostTestClassInfo out of class file scope") - private var _outerMostTestClassContext: TestClassContext? = null + private var _outerMostTestClassContext: TestClassContext? = codeGenLanguage.outerMostTestClassContent /** * This property cannot be accessed outside of test class scope @@ -487,9 +479,9 @@ internal data class CgContext( private var _currentTestClassContext: TestClassContext? = null override val outerMostTestClass: ClassId by lazy { - val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" - val simpleName = testClassCustomName ?: "${classUnderTest.simpleName}Test" - val name = "$packagePrefix$simpleName" + val (simpleName, name) = codeGenLanguage.testClassName( + testClassCustomName, testClassPackageName, classUnderTest + ) BuiltinClassId( name = name, canonicalName = name, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt index 50254afc03..46b7b92597 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/name/CgNameGenerator.kt @@ -14,7 +14,7 @@ import org.utbot.framework.plugin.api.util.isArray /** * Interface for method and variable name generators */ -internal interface CgNameGenerator { +interface CgNameGenerator { /** * Generate a variable name given a [base] name. * @param isMock denotes whether a variable represents a mock object or not @@ -67,7 +67,7 @@ internal interface CgNameGenerator { * Class that generates names for methods and variables * To avoid name collisions it uses existing names information from CgContext */ -internal class CgNameGeneratorImpl(private val context: CgContext) +open class CgNameGeneratorImpl(val context: CgContext) : CgNameGenerator, CgContextOwner by context { override fun variableName(base: String, isMock: Boolean, isStatic: Boolean): String { @@ -78,7 +78,7 @@ internal class CgNameGeneratorImpl(private val context: CgContext) } return when { baseName in existingVariableNames -> nextIndexedVarName(baseName) - isLanguageKeyword(baseName, codegenLanguage) -> createNameFromKeyword(baseName) + isLanguageKeyword(baseName, context.codeGenLanguage) -> createNameFromKeyword(baseName) else -> baseName }.also { existingVariableNames = existingVariableNames.add(it) @@ -130,7 +130,7 @@ internal class CgNameGeneratorImpl(private val context: CgContext) /** * Creates a new indexed variable name by [base] name. */ - private fun nextIndexedVarName(base: String): String = + fun nextIndexedVarName(base: String): String = infiniteInts() .map { "$base$it" } .first { it !in existingVariableNames } @@ -140,20 +140,20 @@ internal class CgNameGeneratorImpl(private val context: CgContext) * * @param skipOne shows if we add "1" to first method name or not */ - private fun nextIndexedMethodName(base: String, skipOne: Boolean = false): String = + fun nextIndexedMethodName(base: String, skipOne: Boolean = false): String = infiniteInts() .map { if (skipOne && it == 1) base else "$base$it" } .first { it !in existingMethodNames } - private fun createNameFromKeyword(baseName: String): String = when(codegenLanguage) { - CodegenLanguage.JAVA -> nextIndexedVarName(baseName) + fun createNameFromKeyword(baseName: String): String = when(codegenLanguage) { CodegenLanguage.KOTLIN -> { // use backticks for first variable with keyword name and use indexed names for all next such variables if (baseName !in existingVariableNames) "`$baseName`" else nextIndexedVarName(baseName) } + else -> nextIndexedVarName(baseName) } - private fun createExecutableName(executableId: ExecutableId): String { + fun createExecutableName(executableId: ExecutableId): String { return when (executableId) { is ConstructorId -> executableId.classId.prettifiedName // TODO: maybe we need some suffix e.g. "Ctor"? is MethodId -> executableId.name diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt index 2c73f18281..3270d44e88 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgCallableAccessManager.kt @@ -83,7 +83,7 @@ interface CgCallableAccessManager { operator fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess } -internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, +class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, CgContextOwner by context { private val statementConstructor by lazy { getStatementConstructorBy(context) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt index 1f0f70ff40..447fcd7356 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt @@ -33,7 +33,7 @@ import org.utbot.framework.util.hasThisInstance import org.utbot.fuzzer.UtFuzzedExecution import java.lang.reflect.Array -internal interface CgFieldStateManager { +interface CgFieldStateManager { fun rememberInitialEnvironmentState(info: StateModificationInfo) fun rememberFinalEnvironmentState(info: StateModificationInfo) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt index f6bdc6a901..b5a8633569 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt @@ -149,14 +149,14 @@ import org.utbot.framework.UtSettings private const val DEEP_EQUALS_MAX_DEPTH = 5 // TODO move it to plugin settings? -internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, +open class CgMethodConstructor(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context), CgStatementConstructor by getStatementConstructorBy(context) { - private val nameGenerator = getNameGeneratorBy(context) - private val testFrameworkManager = getTestFrameworkManagerBy(context) + protected val nameGenerator = getNameGeneratorBy(context) + protected val testFrameworkManager = getTestFrameworkManagerBy(context) - private val variableConstructor = getVariableConstructorBy(context) + protected val variableConstructor = getVariableConstructorBy(context) private val mockFrameworkManager = getMockFrameworkManagerBy(context) private val floatDelta: Float = 1e-6f @@ -164,13 +164,13 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c // a model for execution result (it is lateinit because execution can fail, // and we need it only on assertions generation stage - private lateinit var resultModel: UtModel + lateinit var resultModel: UtModel - private lateinit var methodType: CgTestMethodType + lateinit var methodType: CgTestMethodType private val fieldsOfExecutionResults = mutableMapOf, MutableList>() - private fun setupInstrumentation() { + protected fun setupInstrumentation() { if (currentExecution is UtSymbolicExecution) { val execution = currentExecution as UtSymbolicExecution val instrumentation = execution.instrumentation @@ -215,7 +215,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * Thus, this method only caches an actual initial static fields state in order to recover it * at the end of the test, and it has nothing to do with the 'before' and 'after' caches. */ - private fun rememberInitialStaticFields(statics: Map) { + protected fun rememberInitialStaticFields(statics: Map) { val accessibleStaticFields = statics.accessibleFields() for ((field, _) in accessibleStaticFields) { val declaringClass = field.declaringClass @@ -240,7 +240,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { + protected fun substituteStaticFields(statics: Map, isParametrized: Boolean = false) { val accessibleStaticFields = statics.accessibleFields() for ((field, model) in accessibleStaticFields) { val declaringClass = field.declaringClass @@ -263,7 +263,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun recoverStaticFields() { + protected fun recoverStaticFields() { for ((field, prevValue) in prevStaticFieldValues.accessibleFields()) { if (field.canBeSetFrom(context)) { field.declaringClass[field] `=` prevValue @@ -279,7 +279,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c /** * Generates result assertions for unit tests. */ - private fun generateResultAssertions() { + protected open fun generateResultAssertions() { when (currentExecutable) { is ConstructorId -> generateConstructorCall(currentExecutable!!, currentExecution!!) is BuiltinMethodId -> error("Unexpected BuiltinMethodId $currentExecutable while generating result assertions") @@ -350,7 +350,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { + protected fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { if (exception is AccessControlException) return false // tests with timeout or crash should be processed differently if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false @@ -361,11 +361,11 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return exceptionRequiresAssert || exceptionIsExplicit } - private fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { + protected fun shouldTestPassWithTimeoutException(execution: UtExecution, exception: Throwable): Boolean { return execution.result is UtTimeoutException || exception is TimeoutException } - private fun writeWarningAboutTimeoutExceeding() { + protected fun writeWarningAboutTimeoutExceeding() { +CgMultilineComment( listOf( "This execution may take longer than the ${hangingTestsTimeout.timeoutMs} ms timeout", @@ -374,7 +374,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c ) } - private fun writeWarningAboutFailureTest(exception: Throwable) { + protected fun writeWarningAboutFailureTest(exception: Throwable) { require(currentExecutable is ExecutableId) val executableName = "${currentExecutable!!.classId.name}.${currentExecutable!!.name}" @@ -401,7 +401,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return this.replace("\b", "\\b").replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r") } - private fun writeWarningAboutCrash() { + protected fun writeWarningAboutCrash() { +CgSingleLineComment("This invocation possibly crashes JVM") } @@ -442,7 +442,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * * Note: not supported in parameterized tests. */ - private fun generateFieldStateAssertions() { + protected fun generateFieldStateAssertions() { val thisInstanceCache = statesCache.thisInstance for (path in thisInstanceCache.paths) { assertStatesByPath(thisInstanceCache, path) @@ -694,6 +694,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c // Unit result is considered in generateResultAssertions method error("Unexpected UtVoidModel in deep equals") } + else -> {} } } } @@ -957,6 +958,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c is UtVoidModel -> { // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse } + else -> {} } } } @@ -998,6 +1000,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c is UtVoidModel -> { // only [UtCompositeModel] and [UtAssembleModel] have fields to traverse } + else -> {} } } @@ -1069,7 +1072,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return ClassIdArrayInfo(classId, nestedElementClassId, dimensions) } - private fun assertEquality(expected: CgValue, actual: CgVariable) { + protected fun assertEquality(expected: CgValue, actual: CgVariable) { when { expected.type.isArray -> { // TODO: How to compare arrays of Float and Double wrappers? @@ -1224,7 +1227,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c ) } - private fun recordActualResult() { + protected fun recordActualResult() { currentExecution!!.result.onSuccess { result -> when (val executable = currentExecutable) { is ConstructorId -> { @@ -1250,7 +1253,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + protected fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = withTestMethodScope(execution) { val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) // TODO: remove this line when SAT-1273 is completed @@ -1582,7 +1585,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return arguments } - private fun withTestMethodScope(execution: UtExecution, block: () -> R): R { + protected fun withTestMethodScope(execution: UtExecution, block: () -> R): R { clearTestMethodScope() currentExecution = execution determineExecutionType() @@ -1673,7 +1676,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - private fun testMethod( + protected fun testMethod( methodName: String, displayName: String?, params: List = emptyList(), @@ -1823,7 +1826,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * in order to wrap these calls in a try-catch block that will handle [InvocationTargetException] * that may be thrown by these calls. */ - private fun CgExecutableCall.intercepted() { + protected fun CgExecutableCall.intercepted() { val executableToWrap = when (executableId) { is MethodId -> invoke is ConstructorId -> newInstance diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index a404b2826f..fad443f048 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -44,7 +44,7 @@ import org.utbot.framework.plugin.api.UtMethodTestSet import org.utbot.framework.plugin.api.util.description import org.utbot.framework.plugin.api.util.humanReadableName -internal class CgTestClassConstructor(val context: CgContext) : +open class CgTestClassConstructor(val context: CgContext) : CgContextOwner by context, CgStatementConstructor by getStatementConstructorBy(context) { @@ -52,16 +52,16 @@ internal class CgTestClassConstructor(val context: CgContext) : clearContextRelatedStorage() } - private val methodConstructor = getMethodConstructorBy(context) + protected val methodConstructor = getMethodConstructorBy(context) private val nameGenerator = getNameGeneratorBy(context) - private val testFrameworkManager = getTestFrameworkManagerBy(context) + protected val testFrameworkManager = getTestFrameworkManagerBy(context) - private val testsGenerationReport: TestsGenerationReport = TestsGenerationReport() + protected val testsGenerationReport: TestsGenerationReport = TestsGenerationReport() /** * Given a testClass model constructs CgTestClass */ - fun construct(testClassModel: TestClassModel): CgTestClassFile { + open fun construct(testClassModel: TestClassModel): CgTestClassFile { return buildTestClassFile { this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } imports += context.collectedImports @@ -69,7 +69,7 @@ internal class CgTestClassConstructor(val context: CgContext) : } } - private fun constructTestClass(testClassModel: TestClassModel): CgTestClass { + open fun constructTestClass(testClassModel: TestClassModel): CgTestClass { return buildTestClass { id = currentTestClass @@ -130,7 +130,7 @@ internal class CgTestClassConstructor(val context: CgContext) : } } - private fun constructTestSet(testSet: CgMethodTestSet): List>? { + open fun constructTestSet(testSet: CgMethodTestSet): List>? { if (testSet.executions.isEmpty()) { return null } @@ -189,7 +189,7 @@ internal class CgTestClassConstructor(val context: CgContext) : return regions } - private fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { + protected fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { codeGenerationErrors .getOrPut(testSet) { mutableMapOf() } .merge(failure.description, 1, Int::plus) @@ -290,7 +290,7 @@ internal class CgTestClassConstructor(val context: CgContext) : /** * Engine errors + codegen errors for a given [UtMethodTestSet] */ - private val CgMethodTestSet.allErrors: Map + protected val CgMethodTestSet.allErrors: Map get() = errors + codeGenerationErrors.getOrDefault(this, mapOf()) internal object CgComponents { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt index 6f13d036ba..56cd08a28b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt @@ -74,7 +74,7 @@ import org.utbot.framework.plugin.api.util.wrapperByPrimitive * Constructs CgValue or CgVariable given a UtModel */ @Suppress("unused") -internal class CgVariableConstructor(val context: CgContext) : +open class CgVariableConstructor(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context), CgStatementConstructor by getStatementConstructorBy(context) { @@ -105,7 +105,7 @@ internal class CgVariableConstructor(val context: CgContext) : * We use [valueByModelId] for [UtReferenceModel] by id to not create new variable in case state before * was not transformed. */ - fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue { + open fun getOrCreateVariable(model: UtModel, name: String? = null): CgValue { // name could be taken from existing names, or be specified manually, or be created from generator val baseName = name ?: nameGenerator.nameFrom(model.classId) return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) { @@ -123,6 +123,7 @@ internal class CgVariableConstructor(val context: CgContext) : is UtPrimitiveModel -> CgLiteral(model.classId, model.value) is UtReferenceModel -> error("Unexpected UtReferenceModel: ${model::class}") is UtVoidModel -> error("Unexpected UtVoidModel: ${model::class}") + else -> error("Unexpected UtModel: ${model::class}") } } } @@ -208,7 +209,7 @@ internal class CgVariableConstructor(val context: CgContext) : return obj } - private fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue { + fun constructAssemble(model: UtAssembleModel, baseName: String?): CgValue { val instantiationCall = model.instantiationCall processInstantiationStatement(model, instantiationCall, baseName) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt index 2aaa63a50c..981f2e024c 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/MockFrameworkManager.kt @@ -65,7 +65,7 @@ import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.voidClassId -internal abstract class CgVariableConstructorComponent(val context: CgContext) : +abstract class CgVariableConstructorComponent(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by CgCallableAccessManagerImpl(context), CgStatementConstructor by CgStatementConstructorImpl(context) { @@ -114,12 +114,13 @@ internal abstract class CgVariableConstructorComponent(val context: CgContext) : argumentMatchersClassId[anyOfClass](getClassOf(id)) } -internal class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { +class MockFrameworkManager(context: CgContext) : CgVariableConstructorComponent(context) { private val objectMocker = MockitoMocker(context) private val staticMocker = when (context.staticsMocking) { is NoStaticMocking -> null is MockitoStaticMocking -> MockitoStaticMocker(context, objectMocker) + else -> null } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt index fec6458c1a..e956e50f30 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/TestFrameworkManager.kt @@ -50,7 +50,7 @@ import org.utbot.framework.plugin.api.util.stringClassId import java.util.concurrent.TimeUnit @Suppress("MemberVisibilityCanBePrivate") -internal abstract class TestFrameworkManager(val context: CgContext) +abstract class TestFrameworkManager(val context: CgContext) : CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context) { @@ -239,6 +239,8 @@ internal abstract class TestFrameworkManager(val context: CgContext) fun addDataProvider(dataProvider: CgMethod) { dataProviderMethodsHolder.cgDataProviderMethods += dataProvider } + + open fun assertIsinstance(types: List, actual: CgVariable) {} } internal class TestNgManager(context: CgContext) : TestFrameworkManager(context) { @@ -386,6 +388,7 @@ internal class TestNgManager(context: CgContext) : TestFrameworkManager(context) } } + internal class Junit4Manager(context: CgContext) : TestFrameworkManager(context) { private val parametrizedTestsNotSupportedError: Nothing get() = error("Parametrized tests are not supported for JUnit4") @@ -399,12 +402,14 @@ internal class Junit4Manager(context: CgContext) : TestFrameworkManager(context) override val annotationForOuterClasses: CgAnnotation get() { require(testFramework is Junit4) { "According to settings, JUnit4 was expected, but got: $testFramework" } + require(codegenLanguage == CodegenLanguage.JAVA || codegenLanguage == CodegenLanguage.KOTLIN) { "Expected Java or Kotlin language, but got: $codegenLanguage" } return statementConstructor.annotation( testFramework.runWithAnnotationClassId, testFramework.enclosedClassId.let { when (codegenLanguage) { CodegenLanguage.JAVA -> CgGetJavaClass(it) CodegenLanguage.KOTLIN -> CgGetKotlinClass(it) + else -> throw UnsupportedOperationException() } } ) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt index 5908cd6d27..c40bd7318b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/CgStatementConstructor.kt @@ -186,7 +186,7 @@ interface CgStatementConstructor { fun wrapTypeIfRequired(baseType: ClassId): ClassId } -internal class CgStatementConstructorImpl(context: CgContext) : +class CgStatementConstructorImpl(context: CgContext) : CgStatementConstructor, CgContextOwner by context, CgCallableAccessManager by getCallableAccessManagerBy(context) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt index d25567d86b..77eb837d47 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -46,6 +46,7 @@ import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.WildcardTypeParameter import org.utbot.framework.plugin.api.util.isStatic +import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.util.arrayLikeName import org.utbot.framework.plugin.api.util.builtinStaticMethodId import org.utbot.framework.plugin.api.util.denotableType @@ -53,7 +54,7 @@ import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId -internal data class EnvironmentFieldStateCache( +data class EnvironmentFieldStateCache( val thisInstance: FieldStateCache, val arguments: Array, val classesWithStaticFields: MutableMap @@ -99,7 +100,7 @@ internal data class EnvironmentFieldStateCache( } } -internal class FieldStateCache { +class FieldStateCache { val before: MutableMap = mutableMapOf() val after: MutableMap = mutableMapOf() @@ -125,7 +126,7 @@ internal class FieldStateCache { } } -internal data class CgFieldState(val variable: CgVariable, val model: UtModel) +data class CgFieldState(val variable: CgVariable, val model: UtModel) data class ExpressionWithType(val type: ClassId, val expression: CgExpression) @@ -214,7 +215,7 @@ internal const val MAX_ARRAY_INITIALIZER_SIZE = 10 private fun CgContextOwner.doesNotHaveSimpleNameClash(type: ClassId): Boolean = importedClasses.none { it.simpleName == type.simpleName } -internal fun CgContextOwner.importIfNeeded(type: ClassId) { +fun CgContextOwner.importIfNeeded(type: ClassId) { // TODO: for now we consider that tests are generated in the same package as CUT, but this may change val underlyingType = type.underlyingType @@ -252,6 +253,7 @@ internal fun CgContextOwner.importIfNeeded(method: MethodId) { } } + /** * Casts [expression] to [targetType]. * diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt index ce2de69bcb..1f0f4491f7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/tree/CgElement.kt @@ -78,6 +78,7 @@ interface CgElement { is CgTryCatch -> visit(element) is CgInnerBlock -> visit(element) is CgForLoop -> visit(element) + is CgForEachLoop -> visit(element) is CgWhileLoop -> visit(element) is CgDoWhileLoop -> visit(element) is CgBreakStatement -> visit(element) @@ -133,7 +134,7 @@ data class CgRegularClassFile( data class CgTestClassFile( override val imports: List, override val declaredClass: CgTestClass, - val testsGenerationReport: TestsGenerationReport + val testsGenerationReport: TestsGenerationReport, ) : AbstractCgClassFile() sealed class AbstractCgClass : CgElement { @@ -918,7 +919,7 @@ class CgGetLength(val variable: CgVariable) : CgExpression { // Acquisition of java or kotlin class, e.g. MyClass.class in Java, MyClass::class.java in Kotlin or MyClass::class for Kotlin classes -sealed class CgGetClass(val classId: ClassId) : CgReferenceExpression { +open class CgGetClass(val classId: ClassId) : CgReferenceExpression { override val type: ClassId = Class::class.id } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt index 0a905ccdfa..894b9a1fbc 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DependencyPatterns.kt @@ -16,11 +16,13 @@ fun TestFramework.patterns(): Patterns { Junit4 -> junit4ModulePatterns Junit5 -> junit5ModulePatterns TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException() } val libraryPatterns = when (this) { Junit4 -> junit4Patterns Junit5 -> junit5Patterns TestNg -> testNgPatterns + else -> throw UnsupportedOperationException() } return Patterns(moduleLibraryPatterns, libraryPatterns) @@ -32,11 +34,13 @@ fun TestFramework.parametrizedTestsPatterns(): Patterns { Junit4 -> emptyList() Junit5 -> emptyList() // emptyList here because JUnit5 module may not be enough for parametrized tests if :junit-jupiter-params: is not installed TestNg -> testNgModulePatterns + else -> throw UnsupportedOperationException() } val libraryPatterns = when (this) { Junit4 -> emptyList() Junit5 -> junit5ParametrizedTestsPatterns TestNg -> testNgPatterns + else -> throw UnsupportedOperationException() } return Patterns(moduleLibraryPatterns, libraryPatterns) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt index 80583ef6ea..5511efaee5 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/DslUtil.kt @@ -108,4 +108,5 @@ fun Array<*>.resolve(): List = map { it.resolve() } fun classLiteralAnnotationArgument(id: ClassId, codegenLanguage: CodegenLanguage): CgGetClass = when (codegenLanguage) { CodegenLanguage.JAVA -> CgGetJavaClass(id) CodegenLanguage.KOTLIN -> CgGetKotlinClass(id) + else -> throw UnsupportedOperationException() } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt index c117ed9768..dc25cfa77d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/util/TreeUtil.kt @@ -17,6 +17,6 @@ class CgExceptionHandlerBuilder { } } -internal fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler { +fun buildExceptionHandler(init: CgExceptionHandlerBuilder.() -> Unit): CgExceptionHandler { return CgExceptionHandlerBuilder().apply(init).build() } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt index b4d7a3c8d2..a72e40fb70 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt @@ -40,6 +40,7 @@ import org.utbot.framework.codegen.model.tree.CgExecutableCall import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster import org.utbot.framework.codegen.model.tree.CgExpression import org.utbot.framework.codegen.model.tree.CgFieldAccess +import org.utbot.framework.codegen.model.tree.CgForEachLoop import org.utbot.framework.codegen.model.tree.CgForLoop import org.utbot.framework.codegen.model.tree.CgGreaterThan import org.utbot.framework.codegen.model.tree.CgIfStatement @@ -101,9 +102,9 @@ import org.utbot.framework.plugin.api.util.isRefType import org.utbot.framework.plugin.api.util.longClassId import org.utbot.framework.plugin.api.util.shortClassId -internal abstract class CgAbstractRenderer( +abstract class CgAbstractRenderer( val context: CgRendererContext, - val printer: CgPrinter = CgPrinterImpl() + private val printer: CgPrinter = CgPrinterImpl() ) : CgVisitor, CgPrinter by printer { @@ -112,8 +113,8 @@ internal abstract class CgAbstractRenderer( protected abstract val logicalAnd: String protected abstract val logicalOr: String - protected val regionStart: String = "///region" - protected val regionEnd: String = "///endregion" + protected open val regionStart: String = "///region" + protected open val regionEnd: String = "///endregion" protected abstract val language: CodegenLanguage @@ -528,6 +529,8 @@ internal abstract class CgAbstractRenderer( println() } + override fun visit(element: CgForEachLoop) {} + override fun visit(element: CgWhileLoop) { print("while (") element.condition.accept(this) @@ -855,7 +858,7 @@ internal abstract class CgAbstractRenderer( } } - private fun renderClassFileImports(element: AbstractCgClassFile<*>) { + open fun renderClassFileImports(element: AbstractCgClassFile<*>) { val regularImports = element.imports.filterIsInstance() val staticImports = element.imports.filterIsInstance() @@ -935,8 +938,7 @@ internal abstract class CgAbstractRenderer( context: CgContext, printer: CgPrinter = CgPrinterImpl() ): CgAbstractRenderer { - val rendererContext = CgRendererContext.fromCgContext(context) - return makeRenderer(rendererContext, printer) + return context.codeGenLanguage.cgRenderer(context, printer) } fun makeRenderer( @@ -952,6 +954,7 @@ internal abstract class CgAbstractRenderer( return when (context.codegenLanguage) { CodegenLanguage.JAVA -> CgJavaRenderer(context, printer) CodegenLanguage.KOTLIN -> CgKotlinRenderer(context, printer) + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt index 09459b5f8b..9d2f6baf93 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgKotlinRenderer.kt @@ -529,7 +529,7 @@ internal class CgKotlinRenderer(context: CgRendererContext, printer: CgPrinter = } override fun escapeNamePossibleKeywordImpl(s: String): String = - if (isLanguageKeyword(s, context.codegenLanguage)) "`$s`" else s + if (isLanguageKeyword(s, context.codeGenLanguage)) "`$s`" else s override fun renderClassVisibility(classId: ClassId) { when { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt index 92963984cb..414cd81943 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt @@ -5,17 +5,14 @@ import org.utbot.framework.codegen.model.UtilClassKind.Companion.UT_UTILS_PACKAG import org.utbot.framework.codegen.model.constructor.builtin.UtilMethodProvider import org.utbot.framework.codegen.model.constructor.builtin.utUtilsClassId import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.MethodId -import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.* /** * Information from [CgContext] that is relevant for the renderer. * Not all the information from [CgContext] is required to render a class, * so this more lightweight context is created for this purpose. */ -internal class CgRendererContext( +class CgRendererContext( val shouldOptimizeImports: Boolean, val importedClasses: Set, val importedStaticMethods: Set, @@ -26,6 +23,8 @@ internal class CgRendererContext( val mockFrameworkUsed: Boolean, val mockFramework: MockFramework, ) { + + val codeGenLanguage: CodeGenLanguage = CodeGenLanguage.defaultItem companion object { fun fromCgContext(context: CgContext): CgRendererContext { return CgRendererContext( diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt index 1b89761575..9f3f854ae6 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgVisitor.kt @@ -39,6 +39,7 @@ import org.utbot.framework.codegen.model.tree.CgExecutableCall import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster import org.utbot.framework.codegen.model.tree.CgExpression import org.utbot.framework.codegen.model.tree.CgFieldAccess +import org.utbot.framework.codegen.model.tree.CgForEachLoop import org.utbot.framework.codegen.model.tree.CgForLoop import org.utbot.framework.codegen.model.tree.CgGetJavaClass import org.utbot.framework.codegen.model.tree.CgGetKotlinClass @@ -266,4 +267,6 @@ interface CgVisitor { // Empty line fun visit(element: CgEmptyLine): R + + fun visit(element: CgForEachLoop): R } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt index fb51419a41..57724f0872 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt @@ -125,6 +125,7 @@ private fun getEnumConstantByName(visibility: Visibility, language: CodegenLangu } """ } + else -> "" }.trimIndent() private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguage): String = @@ -175,6 +176,7 @@ private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguag } """ } + else -> "" }.trimIndent() private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): String = @@ -209,6 +211,7 @@ private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): St } """ } + else -> "" }.trimIndent() private fun setStaticField(visibility: Visibility, language: CodegenLanguage): String = @@ -260,6 +263,7 @@ private fun setStaticField(visibility: Visibility, language: CodegenLanguage): S } """ } + else -> "" }.trimIndent() private fun setField(visibility: Visibility, language: CodegenLanguage): String = @@ -294,6 +298,7 @@ private fun setField(visibility: Visibility, language: CodegenLanguage): String } """ } + else -> "" }.trimIndent() private fun createArray(visibility: Visibility, language: CodegenLanguage): String = @@ -328,6 +333,7 @@ private fun createArray(visibility: Visibility, language: CodegenLanguage): Stri } """ } + else -> "" }.trimIndent() private fun createInstance(visibility: Visibility, language: CodegenLanguage): String = @@ -350,6 +356,7 @@ private fun createInstance(visibility: Visibility, language: CodegenLanguage): S } """ } + else -> "" }.trimIndent() private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage): String = @@ -372,6 +379,7 @@ private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage) } """ } + else -> "" }.trimIndent() /** @@ -592,6 +600,7 @@ private fun deepEquals( } """.trimIndent() } + else -> "" } private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -634,6 +643,7 @@ private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): } """.trimIndent() } + else -> "" } private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -674,6 +684,7 @@ private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguag } """.trimIndent() } + else -> "" } private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -718,6 +729,7 @@ private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage) } """.trimIndent() } + else -> "" } private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -774,6 +786,7 @@ private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): S } """.trimIndent() } + else -> "" } private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -812,6 +825,7 @@ private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): } """.trimIndent() } + else -> "" } private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = @@ -826,6 +840,7 @@ private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = """ ${visibility by language}fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) """.trimIndent() + else -> "" } private fun buildStaticLambda(visibility: Visibility, language: CodegenLanguage) = @@ -1362,22 +1377,27 @@ private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( Arrays::class.id ) CodegenLanguage.KOTLIN -> listOf(fieldClassId, Arrays::class.id) + else -> emptyList() } arraysDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(java.lang.reflect.Array::class.id, Set::class.id) CodegenLanguage.KOTLIN -> listOf(java.lang.reflect.Array::class.id) + else -> emptyList() } iterablesDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Iterable::class.id, Iterator::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() + else -> emptyList() } streamsDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(java.util.stream.BaseStream::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() + else -> emptyList() } mapsDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Map::class.id, Iterator::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() + else -> emptyList() } hasCustomEqualsMethodId -> emptyList() getArrayLengthMethodId -> listOf(java.lang.reflect.Array::class.id) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt index 182c0c2f9d..595dd883fa 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt @@ -44,6 +44,10 @@ import kotlin.reflect.KClass import org.mockito.Mockito import org.mockito.stubbing.Answer import org.objectweb.asm.Type +import org.utbot.common.withAccessibility +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.framework.plugin.api.js.JsUtModel +import org.utbot.framework.plugin.api.python.PythonModel import org.utbot.engine.util.lambda.CapturedArgument import org.utbot.engine.util.lambda.constructLambda import org.utbot.engine.util.lambda.constructStaticLambda @@ -132,6 +136,10 @@ class MockValueConstructor( is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) + is PythonModel -> TODO() + is GoUtModel -> TODO() + is JsUtModel -> TODO() + else -> UtConcreteValue(null, model.classId.jClass) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt index a2194dedfe..ad6ad87c0f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtModelConstructor.kt @@ -37,7 +37,7 @@ import java.util.IdentityHashMap /** * Represents common interface for model constructors. */ -internal interface UtModelConstructorInterface { +interface UtModelConstructorInterface { /** * Constructs a UtModel from a concrete [value] with a specific [classId]. */ diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt index 196ee58532..b816baf344 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt @@ -5,7 +5,10 @@ import org.utbot.common.doNotRun import org.utbot.common.unreachableBranch import org.utbot.common.workaround import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.framework.plugin.api.js.JsUtModel import org.utbot.framework.plugin.api.MissingState +import org.utbot.framework.plugin.api.python.PythonModel import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel @@ -266,5 +269,8 @@ fun UtModel.accept(visitor: UtModelVisitor, data: D) = visitor.run { is UtPrimitiveModel -> visit(element, data) is UtReferenceModel -> visit(element, data) is UtVoidModel -> visit(element, data) + is PythonModel -> visit(element, data) + is GoUtModel -> error("Unexpected GoUtModel: unsupported") + is JsUtModel -> error("Unexpected JsUtModel: unsupported") } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt index 3a054916b3..73307c6a36 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt @@ -2,6 +2,9 @@ package org.utbot.framework.minimization import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.framework.plugin.api.js.JsUtModel +import org.utbot.framework.plugin.api.python.PythonModel import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel @@ -226,6 +229,10 @@ private fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): I 1 + instantiationCall.calculateSize(used) + modificationsChain.sumOf { it.calculateSize(used) } } is UtCompositeModel -> 1 + fields.values.sumOf { it.calculateSize(used) } + is PythonModel -> TODO() + is GoUtModel -> TODO() + is JsUtModel -> TODO() + else -> 0 is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt new file mode 100644 index 0000000000..f148d72819 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt @@ -0,0 +1,69 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator +import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl +import org.utbot.framework.codegen.model.constructor.tree.* +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext + + +object CodegenLanguageProvider { + val allItems: MutableList = emptyList().toMutableList() +} + +abstract class CodeGenLanguage : CodeGenerationSettingItem { + open val outerMostTestClassContent: TestClassContext? = null + + override val description: String + get() = "Generate unit tests in $displayName" + + abstract val extension: String + + abstract val languageKeywords: Set + + override fun toString(): String = displayName + + abstract fun testClassName(testClassCustomName: String?, testClassPackageName: String, classUnderTest: ClassId): Pair + + enum class OperatingSystem { + WINDOWS, + UNIX; + + companion object { + fun fromSystemProperties(): OperatingSystem { + val osName = System.getProperty("os.name") + return when { + osName.startsWith("Windows") -> WINDOWS + else -> UNIX + } + } + } + } + val operatingSystem: OperatingSystem = OperatingSystem.fromSystemProperties() + + // Get is mandatory because of the initialization order of the inheritors. + // Otherwise, in some cases we could get an incorrect value + companion object : CodeGenerationSettingBox { + override val defaultItem: CodeGenLanguage get() = allItems.first() + override val allItems: List = CodegenLanguageProvider.allItems.toList() + } + + open fun getNameGeneratorBy(context: CgContext): CgNameGenerator = CgNameGeneratorImpl(context) + open fun getCallableAccessManagerBy(context: CgContext): CgCallableAccessManager = CgCallableAccessManagerImpl(context) + open fun getStatementConstructorBy(context: CgContext): CgStatementConstructor = CgStatementConstructorImpl(context) + open fun getVariableConstructorBy(context: CgContext): CgVariableConstructor = CgVariableConstructor(context) + open fun getMethodConstructorBy(context: CgContext): CgMethodConstructor = CgMethodConstructor(context) + abstract fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer + + open val testFrameworks: List = emptyList() + abstract fun managerByFramework(context: CgContext): TestFrameworkManager + abstract val defaultTestFramework: TestFramework + open var memoryObjects: MutableMap = emptyMap().toMutableMap() +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt new file mode 100644 index 0000000000..b9fc202cb7 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt @@ -0,0 +1,51 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.Junit4 +import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.TestNg +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.Junit4Manager +import org.utbot.framework.codegen.model.constructor.tree.Junit5Manager +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.constructor.tree.TestNgManager +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgJavaRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object JavaCodeLanguage : CodeGenLanguage() { + override val displayName: String = "Java" + + override val extension: String + get() = ".java" + + override val languageKeywords: Set = setOf( + "abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const", + "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float", "for", "goto", + "if", "implements", "import", "instanceof", "int", "interface", "long", "native", "new", "package", "private", + "protected", "public", "return", "short", "static", "strictfp", "super", "switch", "synchronized", "this", + "throw", "throws", "transient", "try", "void", "volatile", "while", "null", "false", "true" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgJavaRenderer(context, printer) + + override val testFrameworks = listOf(Junit4, Junit5, TestNg) + override fun managerByFramework(context: CgContext): TestFrameworkManager = when (context.testFramework) { + is Junit4 -> Junit4Manager(context) + is Junit5 -> Junit5Manager(context) + is TestNg -> TestNgManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework: TestFramework = Junit5 +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt new file mode 100644 index 0000000000..a7a2717841 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt @@ -0,0 +1,49 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.codegen.Junit4 +import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.TestNg +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.Junit4Manager +import org.utbot.framework.codegen.model.constructor.tree.Junit5Manager +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.constructor.tree.TestNgManager +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgKotlinRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object KotlinCodeLanguage : CodeGenLanguage() { + override val displayName: String = "Kotlin" + + override val extension: String + get() = ".kt" + + override val languageKeywords: Set = setOf( + "as", "as?", "break", "class", "continue", "do", "else", "false", "for", "fun", "if", "in", "!in", "interface", + "is", "!is", "null", "object", "package", "return", "super", "this", "throw", "true", "try", "typealias", "typeof", + "val", "var", "when", "while" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgKotlinRenderer(context, printer) + + override val testFrameworks = listOf(Junit4, Junit5, TestNg) + + override fun managerByFramework(context: CgContext): TestFrameworkManager = when (context.testFramework) { + is Junit4 -> Junit4Manager(context) + is Junit5 -> Junit5Manager(context) + is TestNg -> TestNgManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Junit5 +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt new file mode 100644 index 0000000000..025204fedb --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/utils/CodeLanguageUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.framework.plugin.api.utils + +import org.utbot.framework.plugin.api.ClassId + +fun testClassNameGenerator( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId +): Pair { + val packagePrefix = if (testClassPackageName.isNotEmpty()) "$testClassPackageName." else "" + val simpleName = testClassCustomName ?: "${classUnderTest.simpleName}Test" + val name = "$packagePrefix$simpleName" + return Pair(name, simpleName) +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt index 7c95e724f2..3c8ec55ff2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt @@ -37,6 +37,7 @@ data class Snippet(val codegenLanguage: CodegenLanguage, var text: String) { when (codegenLanguage) { CodegenLanguage.JAVA -> text.contains("import $fullyQualifiedName;") CodegenLanguage.KOTLIN -> text.contains("import $fullyQualifiedName") + else -> TODO() } fun doesntHaveImport(fullyQualifiedName: String) = !hasImport(fullyQualifiedName) @@ -45,6 +46,7 @@ data class Snippet(val codegenLanguage: CodegenLanguage, var text: String) { when (codegenLanguage) { CodegenLanguage.JAVA -> text.contains("import static $member;") CodegenLanguage.KOTLIN -> text.contains("import $member") + else -> TODO() } fun doesntHaveStaticImport(member: String) = !hasStaticImport(member) diff --git a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt index 5cab79ad50..5afea5d248 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/tests/infrastructure/TestCodeGeneratorPipeline.kt @@ -100,6 +100,7 @@ class TestCodeGeneratorPipeline(private val testFrameworkConfiguration: TestFram ParametrizedTestSource.DO_NOT_PARAMETRIZE -> "fun " ParametrizedTestSource.PARAMETRIZE -> "fun parameterizedTestsFor" } + else -> throw UnsupportedOperationException() } trimmedLine.startsWith(prefix) } diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt index 0d87b21210..3b217f52de 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt @@ -13,7 +13,7 @@ import org.utbot.framework.plugin.api.ExecutableId * @param concreteValues any concrete values to be processed by fuzzer * */ -class FuzzedMethodDescription( +open class FuzzedMethodDescription( val name: String, val returnType: ClassId, val parameters: List, @@ -74,4 +74,31 @@ class FuzzedMethodDescription( executableId.parameters, concreteValues ) +} + +enum class FuzzedOp(val sign: String?) : FuzzedContext { + NONE(null), + EQ("=="), + NE("!="), + GT(">"), + GE(">="), + LT("<"), + LE("<="), + CH(null), // changed or called + ; + + fun isComparisonOp() = this == EQ || this == NE || this == GT || this == GE || this == LT || this == LE + + fun reverseOrNull() : FuzzedOp? = when(this) { + EQ -> NE + NE -> EQ + GT -> LE + LT -> GE + LE -> GT + GE -> LT + else -> null + } + + fun reverseOrElse(another: (FuzzedOp) -> FuzzedOp): FuzzedOp = + reverseOrNull() ?: another(this) } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt index e9ebfca2e9..a3de0163ae 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -23,6 +23,7 @@ import org.utbot.fuzzer.providers.RegexModelProvider import org.utbot.fuzzer.providers.StringConstantModelProvider import java.util.* import java.util.concurrent.atomic.AtomicInteger +import java.util.function.IntSupplier import kotlin.random.Random import org.utbot.fuzzer.providers.DateConstantModelProvider import org.utbot.fuzzer.providers.PrimitiveRandomModelProvider @@ -267,4 +268,9 @@ suspend fun SequenceScope>.yieldMutated( return true } return false +} + +class SimpleIdGenerator : IntSupplier { + private val id = AtomicInteger() + override fun getAsInt() = id.incrementAndGet() } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt index 95ee2af8d0..855e84d42c 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt @@ -21,7 +21,7 @@ object ConstantsModelProvider : ModelProvider { .forEach { (_, value, op) -> sequenceOf( UtPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, - modifyValue(value, op) + modifyValue(value, op as FuzzedContext) ) .filterNotNull() .forEach { m -> diff --git a/utbot-go/README.md b/utbot-go/README.md new file mode 100644 index 0000000000..d7071bd3ff --- /dev/null +++ b/utbot-go/README.md @@ -0,0 +1,117 @@ +# UTBot Go + +## About project + +UTBot Go _**automatically generates unit tests for Go programs**_. Generated tests: + +* provide _high code coverage_ and, as a result, its reliability; +* fixate the current behavior of the code as _regression tests_. + +The core principles of UTBot Go are _**ease of use**_ and _**maximizing code coverage**_. + +*** + +_The project is currently under development._ + +## Features + +At the moment, only the _basic fuzzing technique_ is supported: namely, the execution of functions on predefined values, +depending on the type of parameter. + +At the moment, functions are supported, the parameters of which have _any primitive types_, namely: + +* `bool` +* `int`, `int8`, `int16`, `int32`, `int64` +* `uint`, `uint8`, `uint16`, `uint32`, `uint64` +* `byte`, `rune`, `string` +* `float64`, `float32` +* `complex128`, `complex64` +* `uintptr` + +For floating point types, _correct work with infinities and NaNs_ is also supported. + +Function result types are supported the same as for parameters, but with _support for types that implement `error`_. + +In addition, UTBot Go correctly captures not only errors returned by functions, but also _`panic` cases_. + +Examples of supported functions can be found [here](samples). + +## Install and use easily + +### IntelliJ IDEA plugin + +_Requirements:_ + +* `IntelliJ IDEA (Ultimate Edition)`, compatible with version `2022.1`; +* installed `Go SDK` version compatible with `1.19` or `1.18`; +* installed in IntelliJ IDEA [Go plugin](https://plugins.jetbrains.com/plugin/9568-go), compatible with the IDE + version (it is for this that the `Ultimate` edition of the IDE is needed); +* properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file + must exist. + +Most likely, if you are already developing Go project in IntelliJ IDEA, then you have already met all the requirements. + +_To install the UTBot Go plugin in IntelliJ IDEA:_ + +* just find the latest version of [UnitTestBot](https://plugins.jetbrains.com/plugin/19445-unittestbot) in the plugin + market; +* or download zip archive with `utbot-intellij JAR` + from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2926264476) and install it in IntelliJ IDEA as + follows from plugins section (yes, you need to select the entire downloaded zip archive, it does not need to be + unpacked). + ![](docs/images/install-intellij-plugin-from-disk.png) + +Finally, you can _start using UTBot Go_: open any `.go` file in the IDE and press `alt + u, alt + t`. After +that, a window will appear in which you can configure the test generation settings and start running it in a couple +of clicks. + +[//]: # (See some example screenshots:) + +[//]: # () + +[//]: # (* opened `.go` source code file) + +[//]: # (* test generation configuration window) + +[//]: # (* generated file with tests) + +### CLI application + +_Requirements:_ + +* installed `Java SDK` version `11` or higher; +* installed `Go SDK` version compatible with `1.19` or `1.18`; +* properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file + must exist. + +_To install the UTBot Go CLI application:_ download zip archive containing `utbot-cli JAR` +from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2926264476), then extract its content (JAR file) to a +convenient location. + +Finally, you can _start using UTBot Go_ by running the extracted JAR on the command line. For example, to +find out about all flags of UTBot Go CLI application, run the command as follows (`utbot-cli-2022.8-beta.jar` here is +the path to the extracted JAR). + +```bash +java -jar utbot-cli-2022.8-beta.jar generateGo --help +``` + +_UTBot Go CLI application options:_ + +* `-s, --source TEXT`, _required_: specifies Go source file to generate tests for. +* `-f, --function TEXT`: specifies function name to generate tests for. Can be used multiple times to select multiple + functions at the same time. If no functions are specified, all functions contained in the source file are selected. +* `-g, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` for + some systems. +* `-p, --print-test`: specifies whether a test should be printed out to StdOut. +* `-w, --overwrite`: specifies whether to overwrite the output test file if it already exists. +* `-h, --help`: show help message and exit. + +## Contribute to UTBot Go + +If you want to _take part in the development_ of the project or _learn more_ about how it works, check +out [DEVELOPERS_GUIDE.md](docs/DEVELOPERS_GUIDE.md). + +For the current list of tasks, check out [FUTURE_PLANS.md](docs/FUTURE_PLANS.md). + +Your help and interest is greatly appreciated! diff --git a/utbot-go/build.gradle b/utbot-go/build.gradle new file mode 100644 index 0000000000..1ccc8e3136 --- /dev/null +++ b/utbot-go/build.gradle @@ -0,0 +1,33 @@ +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" + +group 'org.utbot' +version '1.0-SNAPSHOT' + +compileKotlin { + kotlinOptions { + allWarningsAsErrors = false + } +} + +repositories { + mavenCentral() +} + +dependencies { + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + + implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + + implementation 'com.beust:klaxon:5.5' // to read and write JSON + + api project(':utbot-fuzzers') +} + +test { + useJUnitPlatform() +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions.freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" +} \ No newline at end of file diff --git a/utbot-go/docs/DEVELOPERS_GUIDE.md b/utbot-go/docs/DEVELOPERS_GUIDE.md new file mode 100644 index 0000000000..df3dd964e4 --- /dev/null +++ b/utbot-go/docs/DEVELOPERS_GUIDE.md @@ -0,0 +1,13 @@ +# UTBot Go: Developers Guide + +## How UTBot-Go works in general + +_**TODO:**_ pipeline description and scheme. + +## Deep dive: project's codebase and architecture decisions + +_**TODO:**_ class diagrams + more detailed description + some architecture decisions. + +## How to test UTBot Go + +_**TODO:**_ Gradle `runIde` task and building CLI application JAR locally. diff --git a/utbot-go/docs/FUTURE_PLANS.md b/utbot-go/docs/FUTURE_PLANS.md new file mode 100644 index 0000000000..d841237f73 --- /dev/null +++ b/utbot-go/docs/FUTURE_PLANS.md @@ -0,0 +1,13 @@ +# UTBot Go: Future plans + +## Primarily + +_**TODO**_ + +## Afterwards + +_**TODO**_ + +## Maybe in the future + +_**TODO**_ diff --git a/utbot-go/docs/images/install-intellij-plugin-from-disk.png b/utbot-go/docs/images/install-intellij-plugin-from-disk.png new file mode 100644 index 0000000000000000000000000000000000000000..1f0ceee7569933646f0841e623a21959921dccf7 GIT binary patch literal 25500 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 base) & 1 + return a - (i * z) +} + +// CatalanNumber This function returns the `nth` Catalan number +func CatalanNumber(n int) int { + return f.Iterative(n*2) / (f.Iterative(n) * f.Iterative(n+1)) +} + +// Formula This function calculates the n-th fibonacci number using the [formula](https://en.wikipedia.org/wiki/Fibonacci_number#Relation_to_the_golden_ratio) +// Attention! Tests for large values fall due to rounding error of floating point numbers, works well, only on small numbers +func Formula(n uint) uint { + sqrt5 := math.Sqrt(5) + phi := (sqrt5 + 1) / 2 + powPhi := math.Pow(phi, float64(n)) + return uint(powPhi/sqrt5 + 0.5) +} + +// Extended simple extended gcd +func Extended(a, b int64) (int64, int64, int64) { + if a == 0 { + return b, 0, 1 + } + gcd, xPrime, yPrime := Extended(b%a, a) + return gcd, yPrime - (b/a)*xPrime, xPrime +} + +// Lcm returns the lcm of two numbers using the fact that lcm(a,b) * gcd(a,b) = | a * b | +func Lcm(a, b int64) int64 { + return int64(math.Abs(float64(a*b)) / float64(gcd.Iterative(a, b))) +} + +// IterativePower is iterative O(logn) function for pow(x, y) +func IterativePower(n uint, power uint) uint { + var res uint = 1 + for power > 0 { + if (power & 1) != 0 { + res = res * n + } + + power = power >> 1 + n *= n + } + return res +} + +// IsPowOfTwoUseLog This function checks if a number is a power of two using the logarithm. +// The limiting degree can be from 0 to 63. +// See alternatives in the binary package. +func IsPowOfTwoUseLog(number float64) bool { + if number == 0 || math.Round(number) == math.MaxInt64 { + return false + } + log := math.Log2(number) + return log == math.Round(log) +} + +// Reverse function that will take string, +// and returns the reverse of that string. +func Reverse(str string) string { + rStr := []rune(str) + for i, j := 0, len(rStr)-1; i < len(rStr)/2; i, j = i+1, j-1 { + rStr[i], rStr[j] = rStr[j], rStr[i] + } + return string(rStr) +} + +// Parenthesis algorithm checks if every opened parenthesis +// is closed correctly + +// when parcounter is less than 0 is because a closing +// parenthesis is detected without an opening parenthesis +// that surrounds it + +// parcounter will be 0 if all open parenthesis are closed +// correctly +func Parenthesis(text string) bool { + parcounter := 0 + + for _, r := range text { + switch r { + case '(': + parcounter++ + case ')': + parcounter-- + } + if parcounter < 0 { + return false + } + } + return parcounter == 0 +} + +// IsPalindrome checks if text is palindrome +func IsPalindrome(text string) bool { + clean_text := cleanString(text) + var i, j int + rune := []rune(clean_text) + for i = 0; i < len(rune)/2; i++ { + j = len(rune) - 1 - i + if string(rune[i]) != string(rune[j]) { + return false + } + } + return true +} + +func cleanString(text string) string { + clean_text := strings.ToLower(text) + clean_text = strings.Join(strings.Fields(clean_text), "") // Remove spaces + regex, _ := regexp.Compile(`[^\p{L}\p{N} ]+`) // Regular expression for alphanumeric only characters + return regex.ReplaceAllString(clean_text, "") +} + +// Distance Function that gives Levenshtein Distance +func Distance(str1, str2 string, icost, scost, dcost int) int { + row1 := make([]int, len(str2)+1) + row2 := make([]int, len(str2)+1) + + for i := 1; i <= len(str2); i++ { + row1[i] = i * icost + } + + for i := 1; i <= len(str1); i++ { + row2[0] = i * dcost + + for j := 1; j <= len(str2); j++ { + if str1[i-1] == str2[j-1] { + row2[j] = row1[j-1] + } else { + ins := row2[j-1] + icost + del := row1[j] + dcost + sub := row1[j-1] + scost + + if ins < del && ins < sub { + row2[j] = ins + } else if del < sub { + row2[j] = del + } else { + row2[j] = sub + } + } + } + row1, row2 = row2, row1 + } + + return row1[len(row1)-1] +} + +// Generate returns a newly generated password +func Generate(minLength int, maxLength int) string { + var chars = []byte("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+,.?/:;{}[]`~") + + length, err := cryptorand.Int(cryptorand.Reader, big.NewInt(int64(maxLength-minLength))) + if err != nil { + panic(err) // handle this gracefully + } + length.Add(length, big.NewInt(int64(minLength))) + + intLength := int(length.Int64()) + + newPassword := make([]byte, intLength) + randomData := make([]byte, intLength+intLength/4) + clen := byte(len(chars)) + maxrb := byte(256 - (256 % len(chars))) + i := 0 + for { + if _, err := io.ReadFull(cryptorand.Reader, randomData); err != nil { + panic(err) + } + for _, c := range randomData { + if c >= maxrb { + continue + } + newPassword[i] = chars[c%clen] + i++ + if i == intLength { + return string(newPassword) + } + } + } +} + +// MonteCarloPiConcurrent approximates the value of pi using the Monte Carlo method. +// Unlike the MonteCarloPi function (first version), this implementation uses +// goroutines and channels to parallelize the computation. +// More details on the Monte Carlo method available at https://en.wikipedia.org/wiki/Monte_Carlo_method. +// More details on goroutines parallelization available at https://go.dev/doc/effective_go#parallel. +func MonteCarloPiConcurrent(n int) (float64, error) { + numCPU := runtime.GOMAXPROCS(0) + c := make(chan int, numCPU) + pointsToDraw, err := splitInt(n, numCPU) // split the task in sub-tasks of approximately equal sizes + if err != nil { + return 0, err + } + + // launch numCPU parallel tasks + for _, p := range pointsToDraw { + go drawPoints(p, c) + } + + // collect the tasks results + inside := 0 + for i := 0; i < numCPU; i++ { + inside += <-c + } + return float64(inside) / float64(n) * 4, nil +} + +// drawPoints draws n random two-dimensional points in the interval [0, 1), [0, 1) and sends through c +// the number of points which where within the circle of center 0 and radius 1 (unit circle) +func drawPoints(n int, c chan<- int) { + rnd := rand.New(rand.NewSource(time.Now().UnixNano())) + inside := 0 + for i := 0; i < n; i++ { + x, y := rnd.Float64(), rnd.Float64() + if x*x+y*y <= 1 { + inside++ + } + } + c <- inside +} + +// splitInt takes an integer x and splits it within an integer slice of length n in the most uniform +// way possible. +// For example, splitInt(10, 3) will return []int{4, 3, 3}, nil +func splitInt(x int, n int) ([]int, error) { + if x < n { + return nil, fmt.Errorf("x must be < n - given values are x=%d, n=%d", x, n) + } + split := make([]int, n) + if x%n == 0 { + for i := 0; i < n; i++ { + split[i] = x / n + } + } else { + limit := x % n + for i := 0; i < limit; i++ { + split[i] = x/n + 1 + } + for i := limit; i < n; i++ { + split[i] = x / n + } + } + return split, nil +} diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt new file mode 100644 index 0000000000..c38a8e9c40 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoTypesApi.kt @@ -0,0 +1,24 @@ +package org.utbot.go.api + +import org.utbot.framework.plugin.api.go.GoClassId + +/** + * Represents real Go type. + * + * Note that unique identifier of GoTypeId (as for any children of GoClassId) is its name. + */ +class GoTypeId( + name: String, + val implementsError: Boolean = false +) : GoClassId(name) + +// Wraps tuple of several types into one GoClassId. It helps to handle multiple result types of Go functions. +class GoSyntheticMultipleTypesId(val types: List) : + GoClassId("synthetic_multiple_types${types.typesToString()}") { + override fun toString(): String = types.typesToString() +} + +private fun List.typesToString(): String = this.joinToString(separator = ", ", prefix = "(", postfix = ")") + +// There is no void type in Go; therefore, this class solves function returns nothing case. +class GoSyntheticNoTypeId : GoClassId("") \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt new file mode 100644 index 0000000000..b17ce0d207 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtExecutionResultsApi.kt @@ -0,0 +1,18 @@ +package org.utbot.go.api + +import org.utbot.framework.plugin.api.go.GoUtModel + +interface GoUtExecutionResult + +interface GoUtExecutionCompleted : GoUtExecutionResult { + val models: List +} + +data class GoUtExecutionSuccess(override val models: List) : GoUtExecutionCompleted + +data class GoUtExecutionWithNonNilError(override val models: List) : GoUtExecutionCompleted + +data class GoUtPanicFailure( + val panicValue: GoUtModel, + val panicValueSourceGoType: GoTypeId +) : GoUtExecutionResult \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt new file mode 100644 index 0000000000..e62e6f3c24 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtFunctionApi.kt @@ -0,0 +1,49 @@ +package org.utbot.go.api + +import org.utbot.framework.plugin.api.go.GoClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import java.io.File +import java.nio.file.Paths + +data class GoUtFile(val absolutePath: String, val packageName: String) { + val fileName: String get() = File(absolutePath).name + val fileNameWithoutExtension: String get() = File(absolutePath).nameWithoutExtension + val absoluteDirectoryPath: String get() = Paths.get(absolutePath).parent.toString() +} + +data class GoUtFunctionParameter(val name: String, val type: GoTypeId) + +data class GoUtFunction( + val name: String, + val parameters: List, + val resultTypes: List, + val concreteValues: Collection, + private val sourceFile: GoUtFile +) { + val parametersNames: List get() = parameters.map { it.name } + val parametersTypes: List get() = parameters.map { it.type } + + val resultTypesAsGoClassId: GoClassId + get() = if (resultTypes.isEmpty()) GoSyntheticNoTypeId() + else if (resultTypes.size == 1) resultTypes.first() + else GoSyntheticMultipleTypesId(resultTypes) + + fun toFuzzedMethodDescription() = + FuzzedMethodDescription(name, resultTypesAsGoClassId, parametersTypes, concreteValues).apply { + compilableName = name + val names = parametersNames + parameterNameMap = { index -> names.getOrNull(index) } + } +} + +data class GoUtFuzzedFunction(val function: GoUtFunction, val fuzzedParametersValues: List) + +data class GoUtFuzzedFunctionTestCase( + val fuzzedFunction: GoUtFuzzedFunction, + val executionResult: GoUtExecutionResult, +) { + val function: GoUtFunction get() = fuzzedFunction.function + val fuzzedParametersValues: List get() = fuzzedFunction.fuzzedParametersValues +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt new file mode 100644 index 0000000000..c1d112371e --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/GoUtModelsApi.kt @@ -0,0 +1,77 @@ +@file:Suppress("MemberVisibilityCanBePrivate", "CanBeParameter") + +package org.utbot.go.api + +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.go.api.util.goFloat64TypeId +import org.utbot.go.api.util.neverRequiresExplicitCast + +// NEVER and DEPENDS difference is useful in code generation of assert.Equals(...). +enum class ExplicitCastMode { + REQUIRED, NEVER, DEPENDS +} + +open class GoUtPrimitiveModel( + val value: Any, + val typeId: GoTypeId, + requiredImports: Set = emptySet(), + val explicitCastMode: ExplicitCastMode = + if (typeId.neverRequiresExplicitCast) { + ExplicitCastMode.NEVER + } else { + ExplicitCastMode.DEPENDS + } +) : GoUtModel(typeId, requiredImports) { + + override fun toString() = when (explicitCastMode) { + ExplicitCastMode.REQUIRED -> toCastedValueGoCode() + ExplicitCastMode.DEPENDS, ExplicitCastMode.NEVER -> toValueGoCode() + } + + open fun toValueGoCode(): String = "$value" + fun toCastedValueGoCode(): String = "$typeId(${toValueGoCode()})" +} + +class GoUtFloatNaNModel( + typeId: GoTypeId +) : GoUtPrimitiveModel( + "math.NaN()", + typeId, + requiredImports = setOf("math"), + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + } +) + +class GoUtFloatInfModel( + val sign: Int, + typeId: GoTypeId +) : GoUtPrimitiveModel( + "math.Inf($sign)", + typeId, + requiredImports = setOf("math"), + explicitCastMode = if (typeId != goFloat64TypeId) { + ExplicitCastMode.REQUIRED + } else { + ExplicitCastMode.NEVER + } +) + +class GoUtComplexModel( + val realValue: GoUtPrimitiveModel, + val imagValue: GoUtPrimitiveModel, + typeId: GoTypeId, +) : GoUtPrimitiveModel( + "complex($realValue, $imagValue)", + typeId, + requiredImports = realValue.requiredImports + imagValue.requiredImports, + explicitCastMode = ExplicitCastMode.NEVER +) + +class GoUtNilModel( + val typeId: GoTypeId +) : GoUtModel(typeId, emptySet()) { + override fun toString() = "nil" +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt new file mode 100644 index 0000000000..9cce46b0e9 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoTypesApiUtil.kt @@ -0,0 +1,96 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package org.utbot.go.api.util + +import org.utbot.framework.plugin.api.go.GoClassId +import org.utbot.go.api.GoTypeId +import kotlin.reflect.KClass + +val goAnyTypeId = GoTypeId("any") + +val goByteTypeId = GoTypeId("byte") +val goBoolTypeId = GoTypeId("bool") + +val goComplex128TypeId = GoTypeId("complex128") +val goComplex64TypeId = GoTypeId("complex64") + +val goFloat32TypeId = GoTypeId("float32") +val goFloat64TypeId = GoTypeId("float64") + +val goIntTypeId = GoTypeId("int") +val goInt16TypeId = GoTypeId("int16") +val goInt32TypeId = GoTypeId("int32") +val goInt64TypeId = GoTypeId("int64") +val goInt8TypeId = GoTypeId("int8") + +val goRuneTypeId = GoTypeId("rune") // = int32 +val goStringTypeId = GoTypeId("string") + +val goUintTypeId = GoTypeId("uint") +val goUint16TypeId = GoTypeId("uint16") +val goUint32TypeId = GoTypeId("uint32") +val goUint64TypeId = GoTypeId("uint64") +val goUint8TypeId = GoTypeId("uint8") +val goUintPtrTypeId = GoTypeId("uintptr") + +private val goPrimitives = setOf( + goByteTypeId, + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat32TypeId, + goFloat64TypeId, + goIntTypeId, + goInt16TypeId, + goInt32TypeId, + goInt64TypeId, + goInt8TypeId, + goRuneTypeId, + goStringTypeId, + goUintTypeId, + goUint16TypeId, + goUint32TypeId, + goUint64TypeId, + goUint8TypeId, + goUintPtrTypeId, +) + +val GoClassId.isPrimitiveGoType: Boolean + get() = this in goPrimitives + +private val goTypesNeverRequireExplicitCast = setOf( + goBoolTypeId, + goComplex128TypeId, + goComplex64TypeId, + goFloat64TypeId, + goIntTypeId, + goStringTypeId, +) + +val GoTypeId.neverRequiresExplicitCast: Boolean + get() = this in goTypesNeverRequireExplicitCast + +/** + * This method is useful for converting the string representation of a Go value to its more accurate representation. + * For example, to build more proper GoUtPrimitiveModel-s with GoFuzzedFunctionsExecutor. + * Note, that for now such conversion is not required and is done for convenience only. + * + * About corresponding types: int and uint / uintptr types sizes in Go are platform dependent, + * but are supposed to fit in Long and ULong respectively. + */ +val GoTypeId.correspondingKClass: KClass + get() = when (this) { + goByteTypeId, goUint8TypeId -> UByte::class + goBoolTypeId -> Boolean::class + goFloat32TypeId -> Float::class + goFloat64TypeId -> Double::class + goInt16TypeId -> Short::class + goInt32TypeId, goRuneTypeId -> Int::class + goIntTypeId, goInt64TypeId -> Long::class + goInt8TypeId -> Byte::class + goStringTypeId -> String::class + goUint32TypeId -> UInt::class + goUint16TypeId -> UShort::class + goUintTypeId, goUint64TypeId, goUintPtrTypeId -> ULong::class + else -> String::class // default way to hold GoUtPrimitiveModel's value is to use String + } \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt new file mode 100644 index 0000000000..034bacfd4f --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/api/util/GoUtModelsApiUtil.kt @@ -0,0 +1,29 @@ +package org.utbot.go.api.util + +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.go.api.* + +fun getExplicitCastModeForFloatModel( + typeId: GoTypeId, + explicitCastRequired: Boolean, + defaultFloat32Mode: ExplicitCastMode +): ExplicitCastMode { + if (explicitCastRequired) { + return ExplicitCastMode.REQUIRED + } + return when (typeId) { + goFloat32TypeId -> defaultFloat32Mode + goFloat64TypeId -> ExplicitCastMode.NEVER + else -> error("illegal type") + } +} + +fun GoUtModel.isNaNOrInf(): Boolean = this is GoUtFloatNaNModel || this is GoUtFloatInfModel + +fun GoUtModel.doesNotContainNaNOrInf(): Boolean { + if (this.isNaNOrInf()) return false + val asComplexModel = (this as? GoUtComplexModel) ?: return true + return !(asComplexModel.realValue.isNaNOrInf() || asComplexModel.imagValue.isNaNOrInf()) +} + +fun GoUtModel.containsNaNOrInf(): Boolean = !this.doesNotContainNaNOrInf() \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutor.kt b/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutor.kt new file mode 100644 index 0000000000..d856b19b42 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutor.kt @@ -0,0 +1,170 @@ +package org.utbot.go.executor + +import org.utbot.go.api.* +import org.utbot.go.api.util.* +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.parseFromJsonOrFail +import java.io.File + +object GoFuzzedFunctionsExecutor { + + fun executeGoSourceFileFuzzedFunctions( + sourceFile: GoUtFile, + fuzzedFunctions: List, + goExecutableAbsolutePath: String + ): Map { + val fileToExecuteName = createFileToExecuteName(sourceFile) + val rawExecutionResultsFileName = createRawExecutionResultsFileName(sourceFile) + + val sourceFileDir = File(sourceFile.absoluteDirectoryPath) + val fileToExecute = sourceFileDir.resolve(fileToExecuteName) + val rawExecutionResultsFile = sourceFileDir.resolve(rawExecutionResultsFileName) + + val executorTestFunctionName = createExecutorTestFunctionName() + val runGeneratedGoExecutorTestCommand = listOf( + goExecutableAbsolutePath, + "test", + "-run", + executorTestFunctionName + ) + + try { + val fileToExecuteGoCode = GoFuzzedFunctionsExecutorCodeGenerationHelper.generateExecutorTestFileGoCode( + sourceFile, + fuzzedFunctions, + executorTestFunctionName, + rawExecutionResultsFileName + ) + fileToExecute.writeText(fileToExecuteGoCode) + + executeCommandByNewProcessOrFail( + runGeneratedGoExecutorTestCommand, + sourceFileDir, + "functions from $sourceFile" + ) + val rawExecutionResults = parseFromJsonOrFail(rawExecutionResultsFile) + + return fuzzedFunctions.zip(rawExecutionResults.results) + .associate { (fuzzedFunction, rawExecutionResult) -> + val executionResult = convertRawExecutionResultToExecutionResult( + rawExecutionResult, + fuzzedFunction.function.resultTypes + ) + fuzzedFunction to executionResult + } + } finally { + fileToExecute.delete() + rawExecutionResultsFile.delete() + } + } + + private fun createFileToExecuteName(sourceFile: GoUtFile): String { + return "utbot_go_executor_${sourceFile.fileNameWithoutExtension}_test.go" + } + + private fun createRawExecutionResultsFileName(sourceFile: GoUtFile): String { + return "utbot_go_executor_${sourceFile.fileNameWithoutExtension}_test_results.json" + } + + private fun createExecutorTestFunctionName(): String { + return "TestGoFileFuzzedFunctionsByUtGoExecutor" + } + + private object RawValuesCodes { + const val NAN_VALUE = "NaN" + const val POS_INF_VALUE = "+Inf" + const val NEG_INF_VALUE = "-Inf" + const val COMPLEX_PARTS_DELIMITER = "@" + } + + private fun convertRawExecutionResultToExecutionResult( + rawExecutionResult: RawExecutionResult, + functionResultTypes: List + ): GoUtExecutionResult { + if (rawExecutionResult.panicMessage != null) { + val (rawValue, rawGoType, implementsError) = rawExecutionResult.panicMessage + if (rawValue == null) { + return GoUtPanicFailure(GoUtNilModel(goAnyTypeId), goAnyTypeId) + } + val panicValueSourceGoType = GoTypeId(rawGoType, implementsError = implementsError) + val panicValue = if (panicValueSourceGoType.isPrimitiveGoType) { + createGoUtPrimitiveModelFromRawValue(rawValue, panicValueSourceGoType) + } else { + GoUtPrimitiveModel(rawValue, goStringTypeId) + } + return GoUtPanicFailure(panicValue, panicValueSourceGoType) + } + + if (rawExecutionResult.resultRawValues.size != functionResultTypes.size) { + error("Function completed execution must have as many result raw values as result types.") + } + var executedWithNonNilErrorString = false + val resultValues = + rawExecutionResult.resultRawValues.zip(functionResultTypes).map { (resultRawValue, resultType) -> + if (resultType.implementsError && resultRawValue != null) { + executedWithNonNilErrorString = true + } + if (resultRawValue == null) { + GoUtNilModel(resultType) + } else { + // TODO: support errors fairly, i. e. as structs; for now consider them as strings + val nonNilModelTypeId = if (resultType.implementsError) goStringTypeId else resultType + createGoUtPrimitiveModelFromRawValue(resultRawValue, nonNilModelTypeId) + } + } + return if (executedWithNonNilErrorString) { + GoUtExecutionWithNonNilError(resultValues) + } else { + GoUtExecutionSuccess(resultValues) + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun createGoUtPrimitiveModelFromRawValue(rawValue: String, typeId: GoTypeId): GoUtPrimitiveModel { + if (typeId == goFloat64TypeId || typeId == goFloat32TypeId) { + return convertRawFloatValueToGoUtPrimitiveModel(rawValue, typeId) + } + if (typeId == goComplex128TypeId || typeId == goComplex64TypeId) { + val correspondingFloatType = if (typeId == goComplex128TypeId) goFloat64TypeId else goFloat32TypeId + val (realPartModel, imagPartModel) = rawValue.split(RawValuesCodes.COMPLEX_PARTS_DELIMITER).map { + convertRawFloatValueToGoUtPrimitiveModel(it, correspondingFloatType, typeId == goComplex64TypeId) + } + return GoUtComplexModel(realPartModel, imagPartModel, typeId) + } + val value = when (typeId.correspondingKClass) { + UByte::class -> rawValue.toUByte() + Boolean::class -> rawValue.toBoolean() + Float::class -> rawValue.toFloat() + Double::class -> rawValue.toDouble() + Int::class -> rawValue.toInt() + Short::class -> rawValue.toShort() + Long::class -> rawValue.toLong() + Byte::class -> rawValue.toByte() + UInt::class -> rawValue.toUInt() + UShort::class -> rawValue.toUShort() + ULong::class -> rawValue.toULong() + else -> rawValue + } + return GoUtPrimitiveModel(value, typeId) + } + + private fun convertRawFloatValueToGoUtPrimitiveModel( + rawValue: String, + typeId: GoTypeId, + explicitCastRequired: Boolean = false + ): GoUtPrimitiveModel { + return when (rawValue) { + RawValuesCodes.NAN_VALUE -> GoUtFloatNaNModel(typeId) + RawValuesCodes.POS_INF_VALUE -> GoUtFloatInfModel(1, typeId) + RawValuesCodes.NEG_INF_VALUE -> GoUtFloatInfModel(-1, typeId) + else -> { + val typedValue = if (typeId == goFloat64TypeId) rawValue.toDouble() else rawValue.toFloat() + if (explicitCastRequired) { + GoUtPrimitiveModel(typedValue, typeId, explicitCastMode = ExplicitCastMode.REQUIRED) + } else { + GoUtPrimitiveModel(typedValue, typeId) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutorCodeGenerationHelper.kt b/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutorCodeGenerationHelper.kt new file mode 100644 index 0000000000..a23f919d0a --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/executor/GoFuzzedFunctionsExecutorCodeGenerationHelper.kt @@ -0,0 +1,234 @@ +package org.utbot.go.executor + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFuzzedFunction +import org.utbot.go.simplecodegeneration.GoFileCodeBuilder +import org.utbot.go.simplecodegeneration.generateFuzzedFunctionCall +import org.utbot.go.util.goRequiredImports + +internal object GoFuzzedFunctionsExecutorCodeGenerationHelper { + + private val alwaysRequiredImports = setOf("encoding/json", "fmt", "math", "os", "testing", "reflect") + + fun generateExecutorTestFileGoCode( + sourceFile: GoUtFile, + fuzzedFunctions: List, + executorTestFunctionName: String, + rawExecutionResultsFileName: String, + ): String { + val fileCodeBuilder = GoFileCodeBuilder() + + fileCodeBuilder.setPackage(sourceFile.packageName) + + val additionalImports = mutableSetOf() + fuzzedFunctions.forEach { (_, fuzzedParametersValues) -> + fuzzedParametersValues.forEach { additionalImports += it.goRequiredImports } + } + fileCodeBuilder.setImports(alwaysRequiredImports + additionalImports) + + val executorTestFunctionCode = + generateExecutorTestFunctionCode(fuzzedFunctions, executorTestFunctionName, rawExecutionResultsFileName) + fileCodeBuilder.addTopLevelElements( + CodeTemplates.topLevelHelperStructsAndFunctions + listOf(executorTestFunctionCode) + ) + + return fileCodeBuilder.buildCodeString() + } + + // TODO: use more convenient code generation + private fun generateExecutorTestFunctionCode( + fuzzedFunctions: List, + executorTestFunctionName: String, + rawExecutionResultsFileName: String, + ): String { + val codeSb = StringBuilder() + codeSb.append("func $executorTestFunctionName(t *testing.T) {") + codeSb.append("\n\texecutionResults := __UtBotGoExecutorRawExecutionResults__{Results: []__UtBotGoExecutorRawExecutionResult__{") + + fuzzedFunctions.forEach { fuzzedFunction -> + val fuzzedFunctionCall = generateFuzzedFunctionCall(fuzzedFunction) + val function = fuzzedFunction.function + codeSb.append("\n\t\t__executeFunctionForUtBotGoExecutor__(\"${function.name}\", func() []*string {") + if (function.resultTypes.isEmpty()) { + codeSb.append("\n\t\t\t$fuzzedFunctionCall") + codeSb.append("\n\t\t\treturn []*string{}") + } else { + codeSb.append("\n\t\t\treturn __wrapResultValuesForUtBotGoExecutor__($fuzzedFunctionCall)") + } + codeSb.append("\n\t\t}),") + } + + codeSb.append("\n") + codeSb.append( + """ + }} + + jsonBytes, toJsonErr := json.MarshalIndent(executionResults, "", " ") + __checkErrorAndExitToUtBotGoExecutor__(toJsonErr) + + const resultsFilePath = "$rawExecutionResultsFileName" + writeErr := os.WriteFile(resultsFilePath, jsonBytes, os.ModePerm) + __checkErrorAndExitToUtBotGoExecutor__(writeErr) + } + + """.trimIndent() + ) + + return codeSb.toString() + } + + private object CodeTemplates { + + private val panicMessageStruct = """ + type __UtBotGoExecutorRawPanicMessage__ struct { + RawValue *string `json:"rawValue"` + GoTypeName string `json:"goTypeName"` + ImplementsError bool `json:"implementsError"` + } + """.trimIndent() + + private val rawExecutionResultStruct = """ + type __UtBotGoExecutorRawExecutionResult__ struct { + FunctionName string `json:"functionName"` + ResultRawValues []*string `json:"resultRawValues"` + PanicMessage *__UtBotGoExecutorRawPanicMessage__ `json:"panicMessage"` + } + """.trimIndent() + + private val rawExecutionResultsStruct = """ + type __UtBotGoExecutorRawExecutionResults__ struct { + Results []__UtBotGoExecutorRawExecutionResult__ `json:"results"` + } + """.trimIndent() + + private val checkErrorFunction = """ + func __checkErrorAndExitToUtBotGoExecutor__(err error) { + if err != nil { + os.Exit(1) + } + } + """.trimIndent() + + private val convertFloat64ValueToStringFunction = """ + func __convertFloat64ValueToStringForUtBotGoExecutor__(value float64) string { + const outputNaN = "NaN" + const outputPosInf = "+Inf" + const outputNegInf = "-Inf" + switch { + case math.IsNaN(value): + return fmt.Sprint(outputNaN) + case math.IsInf(value, 1): + return fmt.Sprint(outputPosInf) + case math.IsInf(value, -1): + return fmt.Sprint(outputNegInf) + default: + return fmt.Sprintf("%#v", value) + } + } + """.trimIndent() + + private val convertFloat32ValueToStringFunction = """ + func __convertFloat32ValueToStringForUtBotGoExecutor__(value float32) string { + return __convertFloat64ValueToStringForUtBotGoExecutor__(float64(value)) + } + """.trimIndent() + + private val convertValueToStringFunction = """ + func __convertValueToStringForUtBotGoExecutor__(value any) string { + if typedValue, ok := value.(error); ok { + return fmt.Sprintf("%#v", typedValue.Error()) + } + const outputComplexPartsDelimiter = "@" + switch typedValue := value.(type) { + case complex128: + realPartString := __convertFloat64ValueToStringForUtBotGoExecutor__(real(typedValue)) + imagPartString := __convertFloat64ValueToStringForUtBotGoExecutor__(imag(typedValue)) + return fmt.Sprintf("%v%v%v", realPartString, outputComplexPartsDelimiter, imagPartString) + case complex64: + realPartString := __convertFloat32ValueToStringForUtBotGoExecutor__(real(typedValue)) + imagPartString := __convertFloat32ValueToStringForUtBotGoExecutor__(imag(typedValue)) + return fmt.Sprintf("%v%v%v", realPartString, outputComplexPartsDelimiter, imagPartString) + case float64: + return __convertFloat64ValueToStringForUtBotGoExecutor__(typedValue) + case float32: + return __convertFloat32ValueToStringForUtBotGoExecutor__(typedValue) + case string: + return fmt.Sprintf("%#v", typedValue) + default: + return fmt.Sprintf("%v", typedValue) + } + } + """.trimIndent() + + private val convertValueToRawValueFunction = """ + func __convertValueToRawValueForUtBotGoExecutor__(value any) *string { + if value == nil { + return nil + } else { + rawValue := __convertValueToStringForUtBotGoExecutor__(value) + return &rawValue + } + } + """.trimIndent() + + private val getValueRawGoTypeFunction = """ + func __getValueRawGoTypeForUtBotGoExecutor__(value any) string { + return __convertValueToStringForUtBotGoExecutor__(reflect.TypeOf(value)) + } + """.trimIndent() + + private val executeFunctionFunction = """ + func __executeFunctionForUtBotGoExecutor__(functionName string, wrappedFunction func() []*string) ( + executionResult __UtBotGoExecutorRawExecutionResult__, + ) { + executionResult.FunctionName = functionName + executionResult.ResultRawValues = []*string{} + panicked := true + defer func() { + panicMessage := recover() + if panicked { + _, implementsError := panicMessage.(error) + executionResult.PanicMessage = &__UtBotGoExecutorRawPanicMessage__{ + RawValue: __convertValueToRawValueForUtBotGoExecutor__(panicMessage), + GoTypeName: __getValueRawGoTypeForUtBotGoExecutor__(panicMessage), + ImplementsError: implementsError, + } + } else { + executionResult.PanicMessage = nil + } + }() + + rawResultValues := wrappedFunction() + executionResult.ResultRawValues = rawResultValues + panicked = false + + return executionResult + } + """.trimIndent() + + private val wrapResultValuesFunction = """ + //goland:noinspection GoPreferNilSlice + func __wrapResultValuesForUtBotGoExecutor__(values ...any) []*string { + rawValues := []*string{} + for _, value := range values { + rawValues = append(rawValues, __convertValueToRawValueForUtBotGoExecutor__(value)) + } + return rawValues + } + """.trimIndent() + + val topLevelHelperStructsAndFunctions = listOf( + panicMessageStruct, + rawExecutionResultStruct, + rawExecutionResultsStruct, + checkErrorFunction, + convertFloat64ValueToStringFunction, + convertFloat32ValueToStringFunction, + convertValueToStringFunction, + convertValueToRawValueFunction, + getValueRawGoTypeFunction, + executeFunctionFunction, + wrapResultValuesFunction, + ) + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/executor/RawExecutionResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/executor/RawExecutionResults.kt new file mode 100644 index 0000000000..57653e8509 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/executor/RawExecutionResults.kt @@ -0,0 +1,11 @@ +package org.utbot.go.executor + +internal data class RawPanicMessage(val rawValue: String?, val goTypeName: String, val implementsError: Boolean) + +internal data class RawExecutionResult( + val functionName: String, + val resultRawValues: List, + val panicMessage: RawPanicMessage? +) + +internal data class RawExecutionResults(val results: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/GoFuzzer.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/GoFuzzer.kt new file mode 100644 index 0000000000..77fa7b507f --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/GoFuzzer.kt @@ -0,0 +1,33 @@ +package org.utbot.go.fuzzer + +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.fuzz +import org.utbot.go.api.GoUtFunction +import org.utbot.go.fuzzer.providers.GoConstantsModelProvider +import org.utbot.go.fuzzer.providers.GoPrimitivesModelProvider +import org.utbot.go.fuzzer.providers.GoStringConstantModelProvider + +object GoFuzzer { + + fun goFuzzing(function: GoUtFunction): Sequence> { + + /** + * Unit test generation for functions or methods with no parameters can be useful: + * one can fixate panic behaviour or its absence. + */ + if (function.parameters.isEmpty()) { + return sequenceOf(emptyList()) + } + + // TODO: add more ModelProvider-s + val modelProviderWithFallback = ModelProvider.of( + GoConstantsModelProvider, + GoStringConstantModelProvider, + GoPrimitivesModelProvider + ) + + return fuzz(function.toFuzzedMethodDescription(), modelProviderWithFallback) + } + +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantsModelProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantsModelProvider.kt new file mode 100644 index 0000000000..cd53cc2804 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantsModelProvider.kt @@ -0,0 +1,58 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.framework.plugin.api.go.GoClassId +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.fuzzer.* +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.go.api.GoTypeId +import org.utbot.go.api.GoUtPrimitiveModel + +// This class is a copy of ConstantsModelProvider up to GoClassId.isPrimitive and GoUtPrimitiveModel usage. +@Suppress("DuplicatedCode") +object GoConstantsModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.concreteValues + .asSequence() + .filter { (classId, _, _) -> (classId as GoClassId).isPrimitive } + .forEach { (classId, value, op) -> + sequenceOf( + GoUtPrimitiveModel(value, classId as GoTypeId).fuzzed { summary = "%var% = $value" }, + modifyValue(value, op as FuzzedOp) + ) + .filterNotNull() + .forEach { m -> + description.parametersMap.getOrElse(m.model.classId) { emptyList() }.forEach { index -> + yieldValue(index, m) + } + } + } + } + + // TODO: rewrite with use of GoUtPrimitiveModel + private fun modifyValue(value: Any, op: FuzzedOp): FuzzedValue? { + if (!op.isComparisonOp()) return null + val multiplier = if (op == FuzzedOp.LT || op == FuzzedOp.GE) -1 else 1 + // TODO: add unsigned and other primitive types? + return when (value) { + is Boolean -> value.not() + is Byte -> value + multiplier.toByte() + is Char -> (value.toInt() + multiplier).toChar() + is Short -> value + multiplier.toShort() + is Int -> value + multiplier + is Long -> value + multiplier.toLong() + is Float -> value + multiplier.toDouble() + is Double -> value + multiplier.toDouble() + else -> null + }?.let { + UtPrimitiveModel(it).fuzzed { + summary = "%var% ${ + (if (op == FuzzedOp.EQ || op == FuzzedOp.LE || op == FuzzedOp.GE) { + op.reverseOrNull() ?: error("cannot find reverse operation for $op") + } else op).sign + } $value" + } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesModelProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesModelProvider.kt new file mode 100644 index 0000000000..904d0356dd --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoPrimitivesModelProvider.kt @@ -0,0 +1,142 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.go.api.* +import org.utbot.go.api.util.* + +// This class is highly based on PrimitiveDefaultsModelProvider. +object GoPrimitivesModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.parametersMap.forEach { (classId, parameterIndices) -> + val typeId = classId as? GoTypeId ?: return@forEach + + val primitives: List = when (typeId) { + goByteTypeId -> generateUnsignedIntegerModels(typeId, goUint8TypeId.name) + + goBoolTypeId -> listOf( + GoUtPrimitiveModel(false, typeId).fuzzed { summary = "%var% = false" }, + GoUtPrimitiveModel(true, typeId).fuzzed { summary = "%var% = true" } + ) + + goComplex128TypeId, goComplex64TypeId -> generateComplexModels(typeId) + + goFloat32TypeId, goFloat64TypeId -> generateFloatModels(typeId) + + goIntTypeId, goInt16TypeId, goInt32TypeId, goInt64TypeId, goInt8TypeId -> + generateSignedIntegerModels(typeId) + + goRuneTypeId -> generateSignedIntegerModels(typeId, goInt32TypeId.name) + + goStringTypeId -> listOf( + GoUtPrimitiveModel("\"\"", typeId).fuzzed { summary = "%var% = empty string" }, + GoUtPrimitiveModel("\" \"", typeId).fuzzed { summary = "%var% = blank string" }, + GoUtPrimitiveModel("\"string\"", typeId).fuzzed { summary = "%var% != empty string" }, + GoUtPrimitiveModel("\"\\n\\t\\r\"", typeId).fuzzed { summary = "%var% has special characters" }, + ) + + goUintTypeId, goUint16TypeId, goUint32TypeId, goUint64TypeId, goUint8TypeId -> + generateUnsignedIntegerModels(typeId) + + goUintPtrTypeId -> listOf( + GoUtPrimitiveModel(0, typeId).fuzzed { summary = "%var% = 0" }, + GoUtPrimitiveModel(1, typeId).fuzzed { summary = "%var% > 0" }, + ) + + else -> emptyList() + } + + primitives.forEach { model -> + parameterIndices.forEach { index -> + yieldValue(index, model) + } + } + } + } + + private fun generateSignedIntegerModels(typeId: GoTypeId, mathTypeName: String = typeId.name): List { + val minValue = "math.Min${mathTypeName.capitalize()}" + val maxValue = "math.Max${mathTypeName.capitalize()}" + return listOf( + GoUtPrimitiveModel(0, typeId).fuzzed { summary = "%var% = 0" }, + GoUtPrimitiveModel(1, typeId).fuzzed { summary = "%var% > 0" }, + GoUtPrimitiveModel(-1, typeId).fuzzed { summary = "%var% < 0" }, + GoUtPrimitiveModel(minValue, typeId, setOf("math")).fuzzed { summary = "%var% = $minValue" }, + GoUtPrimitiveModel(maxValue, typeId, setOf("math")).fuzzed { summary = "%var% = $maxValue" }, + ) + } + + private fun generateUnsignedIntegerModels(typeId: GoTypeId, mathTypeName: String = typeId.name): List { + val maxValue = "math.Max${mathTypeName.capitalize()}" + return listOf( + GoUtPrimitiveModel(0, typeId).fuzzed { summary = "%var% = 0" }, + GoUtPrimitiveModel(1, typeId).fuzzed { summary = "%var% > 0" }, + GoUtPrimitiveModel(maxValue, typeId, setOf("math")).fuzzed { summary = "%var% = $maxValue" }, + ) + } + + private fun generateFloatModels( + typeId: GoTypeId, + explicitCastRequired: Boolean = false + ): List { + val maxValue = "math.Max${typeId.name.capitalize()}" + val smallestNonZeroValue = "math.SmallestNonzero${typeId.name.capitalize()}" + + val explicitCastRequiredModeIfFloat32 = + getExplicitCastModeForFloatModel(typeId, explicitCastRequired, ExplicitCastMode.REQUIRED) + val explicitCastMode = getExplicitCastModeForFloatModel(typeId, explicitCastRequired, ExplicitCastMode.DEPENDS) + + return listOf( + GoUtPrimitiveModel(0.0, typeId, explicitCastMode = explicitCastMode).fuzzed { + summary = "%var% = 0.0" + }, + GoUtPrimitiveModel(1.1, typeId, explicitCastMode = explicitCastMode).fuzzed { + summary = "%var% > 0.0" + }, + GoUtPrimitiveModel(-1.1, typeId, explicitCastMode = explicitCastMode).fuzzed { + summary = "%var% < 0.0" + }, + GoUtPrimitiveModel( + smallestNonZeroValue, + typeId, + requiredImports = setOf("math"), + explicitCastMode = explicitCastRequiredModeIfFloat32 + ).fuzzed { + summary = "%var% = $smallestNonZeroValue" + }, + GoUtPrimitiveModel( + maxValue, + typeId, + requiredImports = setOf("math"), + explicitCastMode = explicitCastRequiredModeIfFloat32 + ).fuzzed { + summary = "%var% = $maxValue" + }, + GoUtFloatInfModel(-1, typeId).fuzzed { summary = "%var% = math.Inf(-1)" }, + GoUtFloatInfModel(1, typeId).fuzzed { summary = "%var% = math.Inf(1)" }, + GoUtFloatNaNModel(typeId).fuzzed { summary = "%var% = math.NaN()" }, + ) + } + + private fun cartesianProduct(listA: List, listB: List): List> { + val result = mutableListOf>() + listA.forEach { a -> listB.forEach { b -> result.add(listOf(a, b)) } } + return result + } + + private fun generateComplexModels(typeId: GoTypeId): List { + val correspondingFloatType = if (typeId == goComplex128TypeId) goFloat64TypeId else goFloat32TypeId + val componentModels = generateFloatModels(correspondingFloatType, typeId == goComplex64TypeId) + return cartesianProduct(componentModels, componentModels).map { (realFuzzedValue, imagFuzzedValue) -> + GoUtComplexModel( + realFuzzedValue.model as GoUtPrimitiveModel, + imagFuzzedValue.model as GoUtPrimitiveModel, + typeId + ).fuzzed { summary = "%var% = complex(${realFuzzedValue.summary}, ${imagFuzzedValue.summary})" } + } + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStringConstantModelProvider.kt b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStringConstantModelProvider.kt new file mode 100644 index 0000000000..3464ab2a25 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStringConstantModelProvider.kt @@ -0,0 +1,54 @@ +package org.utbot.go.fuzzer.providers + +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.go.api.GoUtPrimitiveModel +import org.utbot.go.api.util.goStringTypeId +import kotlin.random.Random + +// This class is a copy of StringConstantModelProvider up to goStringClassId and GoUtPrimitiveModel usage. +@Suppress("DuplicatedCode") +object GoStringConstantModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val random = Random(72923L) + description.concreteValues + .asSequence() + .filter { (classId, _) -> classId == goStringTypeId } + .forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op as FuzzedOp)) + .asSequence() + .filterNotNull() + .map { GoUtPrimitiveModel(it, goStringTypeId) }.forEach { model -> + description.parametersMap.getOrElse(model.classId) { emptyList() }.forEach { index -> + yieldValue(index, model.fuzzed { summary = "%var% = string" }) + } + } + } + } + + private fun mutate(random: Random, value: String?, op: FuzzedOp): String? { + if (value.isNullOrEmpty() || op != FuzzedOp.CH) return null + val indexOfMutation = random.nextInt(value.length) + return value.replaceRange( + indexOfMutation, + indexOfMutation + 1, + SingleCharacterSequence(value[indexOfMutation] - random.nextInt(1, 128)) + ) + } + + private class SingleCharacterSequence(private val character: Char) : CharSequence { + override val length: Int + get() = 1 + + override fun get(index: Int): Char = character + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + throw UnsupportedOperationException() + } + + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt new file mode 100644 index 0000000000..6089595956 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisResults.kt @@ -0,0 +1,21 @@ +package org.utbot.go.gocodeanalyzer + +internal data class AnalyzedType(val name: String, val implementsError: Boolean) + +internal data class AnalyzedFunctionParameter(val name: String, val type: AnalyzedType) + +internal data class AnalyzedFunction( + val name: String, + val parameters: List, + val resultTypes: List, +) + +internal data class AnalysisResult( + val absoluteFilePath: String, + val packageName: String, + val analyzedFunctions: List, + val notSupportedFunctionsNames: List, + val notFoundFunctionsNames: List +) + +internal data class AnalysisResults(val results: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt new file mode 100644 index 0000000000..0a56357b1b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/AnalysisTargets.kt @@ -0,0 +1,5 @@ +package org.utbot.go.gocodeanalyzer + +internal data class AnalysisTarget(val absoluteFilePath: String, val targetFunctionsNames: List) + +internal data class AnalysisTargets(val targets: List) \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt new file mode 100644 index 0000000000..fb413a76c4 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/gocodeanalyzer/GoSourceCodeAnalyzer.kt @@ -0,0 +1,122 @@ +package org.utbot.go.gocodeanalyzer + +import org.utbot.common.FileUtil.extractDirectoryFromArchive +import org.utbot.common.scanForResourcesContaining +import java.io.File +import org.utbot.go.api.GoTypeId +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFunctionParameter +import org.utbot.go.util.executeCommandByNewProcessOrFail +import org.utbot.go.util.parseFromJsonOrFail +import org.utbot.go.util.writeJsonToFileOrFail + +object GoSourceCodeAnalyzer { + + data class GoSourceFileAnalysisResult( + val functions: List, + val notSupportedFunctionsNames: List, + val notFoundFunctionsNames: List + ) + + /** + * Takes map from absolute paths of Go source files to names of their selected functions. + * If list is empty, all containing functions are selected. + * + * Returns GoSourceFileAnalysisResult-s grouped by their source files. + */ + fun analyzeGoSourceFilesForFunctions( + targetFunctionsNamesBySourceFiles: Map>, + goExecutableAbsolutePath: String + ): Map { + val analysisTargets = AnalysisTargets( + targetFunctionsNamesBySourceFiles.map { (absoluteFilePath, targetFunctionsNames) -> + AnalysisTarget(absoluteFilePath, targetFunctionsNames) + } + ) + val analysisTargetsFileName = createAnalysisTargetsFileName() + val analysisResultsFileName = createAnalysisResultsFileName() + + val goCodeAnalyzerSourceDir = extractGoCodeAnalyzerSourceDirectory() + val analysisTargetsFile = goCodeAnalyzerSourceDir.resolve(analysisTargetsFileName) + val analysisResultsFile = goCodeAnalyzerSourceDir.resolve(analysisResultsFileName) + + val goCodeAnalyzerRunCommand = listOf( + goExecutableAbsolutePath, + "run" + ) + getGoCodeAnalyzerSourceFilesNames() + listOf( + "-targets", + analysisTargetsFileName, + "-results", + analysisResultsFileName, + ) + + try { + writeJsonToFileOrFail(analysisTargets, analysisTargetsFile) + executeCommandByNewProcessOrFail( + goCodeAnalyzerRunCommand, + goCodeAnalyzerSourceDir, + "GoSourceCodeAnalyzer for $analysisTargets" + ) + val analysisResults = parseFromJsonOrFail(analysisResultsFile) + + return analysisResults.results.map { analysisResult -> + GoUtFile(analysisResult.absoluteFilePath, analysisResult.packageName) to analysisResult + }.associateBy({ (sourceFile, _) -> sourceFile }) { (sourceFile, analysisResult) -> + val functions = analysisResult.analyzedFunctions.map { analyzedFunction -> + fun AnalyzedType.toGoTypeId() = GoTypeId(this.name, implementsError = this.implementsError) + val parameters = analyzedFunction.parameters.map { analyzedFunctionParameter -> + GoUtFunctionParameter( + analyzedFunctionParameter.name, + analyzedFunctionParameter.type.toGoTypeId() + ) + } + val resultTypes = analyzedFunction.resultTypes.map { analyzedType -> analyzedType.toGoTypeId() } + GoUtFunction( + analyzedFunction.name, + parameters, + resultTypes, + emptyList(), // TODO: extract concrete values from function's body + sourceFile + ) + } + GoSourceFileAnalysisResult( + functions, + analysisResult.notSupportedFunctionsNames, + analysisResult.notFoundFunctionsNames + ) + } + } finally { + analysisTargetsFile.delete() + analysisResultsFile.delete() + goCodeAnalyzerSourceDir.deleteRecursively() + } + } + + private fun extractGoCodeAnalyzerSourceDirectory(): File { + val sourceDirectoryName = "go_source_code_analyzer" + val classLoader = GoSourceCodeAnalyzer::class.java.classLoader + + val containingResourceFile = classLoader.scanForResourcesContaining(sourceDirectoryName).firstOrNull() + ?: error("Can't find resource containing $sourceDirectoryName directory.") + if (containingResourceFile.extension != "jar") { + error("Resource for $sourceDirectoryName directory is expected to be JAR: others are not supported yet.") + } + + val archiveFilePath = containingResourceFile.toPath() + return extractDirectoryFromArchive(archiveFilePath, sourceDirectoryName)?.toFile() + ?: error("Can't find $sourceDirectoryName directory at the top level of JAR ${archiveFilePath.toAbsolutePath()}.") + } + + private fun getGoCodeAnalyzerSourceFilesNames(): List { + return listOf("main.go", "analyzer_core.go", "analysis_targets.go", "analysis_results.go") + } + + private fun createAnalysisTargetsFileName(): String { + return "ut_go_analysis_targets.json" + } + + private fun createAnalysisResultsFileName(): String { + return "ut_go_analysis_results.json" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..01dbdb0fd0 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/AbstractGoUtTestsGenerationController.kt @@ -0,0 +1,86 @@ +package org.utbot.go.logic + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.simplecodegeneration.GoTestCasesCodeGenerator + +abstract class AbstractGoUtTestsGenerationController(private val goExecutableAbsolutePath: String) { + + fun generateTests(selectedFunctionsNamesBySourceFiles: Map>) { + if (!onSourceCodeAnalysisStart(selectedFunctionsNamesBySourceFiles)) return + + val analysisResults = GoSourceCodeAnalyzer.analyzeGoSourceFilesForFunctions( + selectedFunctionsNamesBySourceFiles, + goExecutableAbsolutePath + ) + if (!onSourceCodeAnalysisFinished(analysisResults)) return + + val testCasesBySourceFiles = analysisResults.mapValues { (sourceFile, analysisResult) -> + val functions = analysisResult.functions + if (!onTestCasesGenerationForGoSourceFileFunctionsStart(sourceFile, functions)) return + GoTestCasesGenerator.generateTestCasesForGoSourceFileFunctions( + sourceFile, + functions, + goExecutableAbsolutePath + ).also { if (!onTestCasesGenerationForGoSourceFileFunctionsFinished(sourceFile, it)) return } + } + + testCasesBySourceFiles.forEach { (sourceFile, testCases) -> + if (!onTestCasesFileCodeGenerationStart(sourceFile, testCases)) return + val generatedTestsFileCode = GoTestCasesCodeGenerator.generateTestCasesFileCode(sourceFile, testCases) + if (!onTestCasesFileCodeGenerationFinished(sourceFile, generatedTestsFileCode)) return + } + } + + protected abstract fun onSourceCodeAnalysisStart(targetFunctionsNamesBySourceFiles: Map>): Boolean + + protected abstract fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean + + protected abstract fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean + + protected abstract fun onTestCasesFileCodeGenerationFinished( + sourceFile: GoUtFile, + generatedTestsFileCode: String + ): Boolean + + protected fun generateMissingSelectedFunctionsListMessage( + analysisResults: Map, + ): String? { + val missingSelectedFunctions = analysisResults.filter { (_, analysisResult) -> + analysisResult.notSupportedFunctionsNames.isNotEmpty() || analysisResult.notFoundFunctionsNames.isNotEmpty() + } + if (missingSelectedFunctions.isEmpty()) { + return null + } + return missingSelectedFunctions.map { (sourceFile, analysisResult) -> + val notSupportedFunctions = analysisResult.notSupportedFunctionsNames.joinToString(separator = ", ") + val notFoundFunctions = analysisResult.notFoundFunctionsNames.joinToString(separator = ", ") + val messageSb = StringBuilder() + messageSb.append("File ${sourceFile.absolutePath}") + if (notSupportedFunctions.isNotEmpty()) { + messageSb.append("\n-- contains currently unsupported functions: $notSupportedFunctions") + } + if (notFoundFunctions.isNotEmpty()) { + messageSb.append("\n-- does not contain functions: $notFoundFunctions") + } + messageSb.toString() + }.joinToString(separator = "\n\n", prefix = "\n\n", postfix = "\n\n") + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt new file mode 100644 index 0000000000..5dbb6bd1bf --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/logic/GoTestCasesGenerator.kt @@ -0,0 +1,32 @@ +package org.utbot.go.logic + +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.executor.GoFuzzedFunctionsExecutor +import org.utbot.go.fuzzer.GoFuzzer + +object GoTestCasesGenerator { + + fun generateTestCasesForGoSourceFileFunctions( + sourceFile: GoUtFile, + functions: List, + goExecutableAbsolutePath: String + ): List { + val fuzzedFunctions = functions.map { function -> + GoFuzzer.goFuzzing(function = function).map { fuzzedParametersValues -> + GoUtFuzzedFunction(function, fuzzedParametersValues) + }.toList() + }.flatten() + + return GoFuzzedFunctionsExecutor.executeGoSourceFileFuzzedFunctions( + sourceFile, + fuzzedFunctions, + goExecutableAbsolutePath + ).map { (fuzzedFunction, executionResult) -> + GoUtFuzzedFunctionTestCase(fuzzedFunction, executionResult) + } + } + +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt new file mode 100644 index 0000000000..12bc3b753d --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoCodeGenerationUtil.kt @@ -0,0 +1,42 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.go.api.ExplicitCastMode +import org.utbot.go.api.GoTypeId +import org.utbot.go.api.GoUtFuzzedFunction +import org.utbot.go.api.GoUtPrimitiveModel + +fun generateFuzzedFunctionCall(fuzzedFunction: GoUtFuzzedFunction): String { + val fuzzedParameters = fuzzedFunction.fuzzedParametersValues.joinToString(separator = ", ") { + (it.model as GoUtPrimitiveModel).toString() + } + return "${fuzzedFunction.function.name}($fuzzedParameters)" +} + +fun generateVariablesDeclarationTo(variablesNames: List, expression: String): String { + val variables = variablesNames.joinToString(separator = ", ") + return "$variables := $expression" +} + +fun generateFuzzedFunctionCallSavedToVariables( + variablesNames: List, + fuzzedFunction: GoUtFuzzedFunction +): String = generateVariablesDeclarationTo( + variablesNames, + generateFuzzedFunctionCall(fuzzedFunction) +) + +fun generateCastIfNeed(toTypeId: GoTypeId, expressionType: GoTypeId, expression: String): String { + return if (expressionType != toTypeId) { + "${toTypeId.name}($expression)" + } else { + expression + } +} + +fun generateCastedValueIfPossible(model: GoUtPrimitiveModel): String { + return if (model.explicitCastMode == ExplicitCastMode.NEVER) { + model.toValueGoCode() + } else { + model.toCastedValueGoCode() + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt new file mode 100644 index 0000000000..0696e8424b --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoFileCodeBuilder.kt @@ -0,0 +1,36 @@ +package org.utbot.go.simplecodegeneration + +class GoFileCodeBuilder { + + private var packageLine: String? = null + private var importLines: String? = null + private val topLevelElements: MutableList = mutableListOf() + + fun buildCodeString(): String { + return "$packageLine\n\n$importLines\n\n${topLevelElements.joinToString(separator = "\n\n")}" + } + + fun setPackage(packageName: String) { + packageLine = "package $packageName" + } + + fun setImports(importNames: Set) { + val sortedImportNames = importNames.toList().sorted() + if (sortedImportNames.isEmpty()) return + if (sortedImportNames.size == 1) { + importLines = "import ${sortedImportNames.first()}" + return + } + importLines = sortedImportNames.joinToString(separator = "", prefix = "import(\n", postfix = ")") { + "\t\"$it\"\n" + } + } + + fun addTopLevelElements(vararg elements: String) { + topLevelElements.addAll(elements) + } + + fun addTopLevelElements(elements: Iterable) { + topLevelElements.addAll(elements) + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt new file mode 100644 index 0000000000..d0e3a85d9f --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/simplecodegeneration/GoTestCasesCodeGenerator.kt @@ -0,0 +1,202 @@ +package org.utbot.go.simplecodegeneration + +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.go.api.* +import org.utbot.go.api.util.* +import org.utbot.go.util.goRequiredImports + +object GoTestCasesCodeGenerator { + + fun generateTestCasesFileCode(sourceFile: GoUtFile, testCases: List): String { + val fileBuilder = GoFileCodeBuilder() + + fileBuilder.setPackage(sourceFile.packageName) + + val imports = mutableSetOf("github.com/stretchr/testify/assert", "testing") + testCases.forEach { testCase -> + testCase.fuzzedParametersValues.forEach { + imports += it.goRequiredImports + } + when (val executionResult = testCase.executionResult) { + is GoUtExecutionCompleted -> { + executionResult.models.forEach { + imports += it.requiredImports + } + } + + is GoUtPanicFailure -> { + imports += executionResult.panicValue.requiredImports + } + } + } + fileBuilder.setImports(imports) + + fun List.generateTestFunctions( + generateTestFunctionForTestCase: (GoUtFuzzedFunctionTestCase, Int?) -> String + ) { + this.forEachIndexed { testIndex, testCase -> + val testIndexToShow = if (this.size == 1) null else testIndex + 1 + val testFunctionCode = generateTestFunctionForTestCase(testCase, testIndexToShow) + fileBuilder.addTopLevelElements(testFunctionCode) + } + } + + fun GoUtFuzzedFunctionTestCase.isPanicTestCase(): Boolean { + return this.executionResult is GoUtPanicFailure + } + + val testCasesByFunction = testCases.groupBy { it.function } + testCasesByFunction.forEach { (_, functionTestCases) -> + functionTestCases.filterNot { it.isPanicTestCase() } + .generateTestFunctions(::generateTestFunctionForCompletedExecutionTestCase) + functionTestCases.filter { it.isPanicTestCase() } + .generateTestFunctions(::generateTestFunctionForPanicFailureTestCase) + } + + return fileBuilder.buildCodeString() + } + + private fun generateTestFunctionForCompletedExecutionTestCase( + testCase: GoUtFuzzedFunctionTestCase, + testIndexToShow: Int? + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testFunctionNamePostfix = + if (executionResult is GoUtExecutionWithNonNilError) { + "WithNonNilError" + } else { + "" + } + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.capitalize()}${testFunctionNamePostfix}ByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + if (function.resultTypes.isEmpty()) { + val actualFunctionCall = generateFuzzedFunctionCall(fuzzedFunction) + val testFunctionBody = "\tassert.NotPanics(t, func() { $actualFunctionCall })\n" + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + val testFunctionBodySb = StringBuilder() + + val resultTypes = function.resultTypes + val doResultTypesImplementError = resultTypes.map { it.implementsError } + val actualResultVariablesNames = run { + val errorVariablesNumber = doResultTypesImplementError.count { it } + val commonVariablesNumber = resultTypes.size - errorVariablesNumber + + var errorVariablesIndex = 0 + var commonVariablesIndex = 0 + doResultTypesImplementError.map { implementsError -> + if (implementsError) { + "actualErr${if (errorVariablesNumber > 1) errorVariablesIndex++ else ""}" + } else { + "actualVal${if (commonVariablesNumber > 1) commonVariablesIndex++ else ""}" + } + } + } + val actualFunctionCall = generateFuzzedFunctionCallSavedToVariables(actualResultVariablesNames, fuzzedFunction) + testFunctionBodySb.append("\t$actualFunctionCall\n\n") + + val expectedModels = (executionResult as GoUtExecutionCompleted).models + val (assertionName, assertionTParameter) = + if (expectedModels.size > 1 || expectedModels.any { it.isComplexModelAndNeedsSeparateAssertions() }) { + testFunctionBodySb.append("\tassertMultiple := assert.New(t)\n") + "assertMultiple" to "" + } else { + "assert" to "t, " + } + actualResultVariablesNames.zip(expectedModels).zip(doResultTypesImplementError) + .forEach { (resultVariableAndModel, doesResultTypeImplementError) -> + val (actualResultVariableName, expectedModel) = resultVariableAndModel + + val assertionCalls = mutableListOf() + fun generateAssertionCallHelper(refinedExpectedModel: GoUtModel, actualResultCode: String) { + val code = generateCompletedExecutionAssertionCall( + refinedExpectedModel, + actualResultCode, + doesResultTypeImplementError, + assertionTParameter + ) + assertionCalls.add(code) + } + + if (expectedModel.isComplexModelAndNeedsSeparateAssertions()) { + val complexModel = expectedModel as GoUtComplexModel + generateAssertionCallHelper(complexModel.realValue, "real($actualResultVariableName)") + generateAssertionCallHelper(complexModel.imagValue, "imag($actualResultVariableName)") + } else { + generateAssertionCallHelper(expectedModel, actualResultVariableName) + } + assertionCalls.forEach { testFunctionBodySb.append("\t$assertionName.$it\n") } + } + val testFunctionBody = testFunctionBodySb.toString() + + return "$testFunctionSignatureDeclaration {\n$testFunctionBody}" + } + + private fun GoUtModel.isComplexModelAndNeedsSeparateAssertions(): Boolean = + this is GoUtComplexModel && this.containsNaNOrInf() + + private fun generateCompletedExecutionAssertionCall( + expectedModel: GoUtModel, + actualResultCode: String, + doesReturnTypeImplementError: Boolean, + assertionTParameter: String + ): String { + if (expectedModel is GoUtNilModel) { + return "Nil($assertionTParameter$actualResultCode)" + } + if (doesReturnTypeImplementError && expectedModel.classId == goStringTypeId) { + return "ErrorContains($assertionTParameter$actualResultCode, $expectedModel)" + } + if (expectedModel is GoUtFloatNaNModel) { + val castedActualResultCode = + generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsNaN($castedActualResultCode))" + } + if (expectedModel is GoUtFloatInfModel) { + val castedActualResultCode = + generateCastIfNeed(goFloat64TypeId, expectedModel.typeId, actualResultCode) + return "True(${assertionTParameter}math.IsInf($castedActualResultCode, ${expectedModel.sign}))" + } + val castedExpectedResultCode = generateCastedValueIfPossible(expectedModel as GoUtPrimitiveModel) + return "Equal($assertionTParameter$castedExpectedResultCode, $actualResultCode)" + } + + private fun generateTestFunctionForPanicFailureTestCase( + testCase: GoUtFuzzedFunctionTestCase, + testIndexToShow: Int? + ): String { + val (fuzzedFunction, executionResult) = testCase + val function = fuzzedFunction.function + + val testIndexToShowString = testIndexToShow ?: "" + val testFunctionSignatureDeclaration = + "func Test${function.name.capitalize()}PanicsByUtGoFuzzer$testIndexToShowString(t *testing.T)" + + val actualFunctionCall = generateFuzzedFunctionCall(fuzzedFunction) + val actualFunctionCallLambda = "func() { $actualFunctionCall }" + val (expectedPanicValue, expectedPanicValueSourceGoType) = (executionResult as GoUtPanicFailure) + + val isPrimitiveWithOkEquals = + expectedPanicValueSourceGoType.isPrimitiveGoType && expectedPanicValue.doesNotContainNaNOrInf() + val testFunctionBody = if (isPrimitiveWithOkEquals || expectedPanicValue is GoUtNilModel) { + val expectedPanicValueCode = if (expectedPanicValue is GoUtNilModel) { + "$expectedPanicValue" + } else { + generateCastedValueIfPossible(expectedPanicValue as GoUtPrimitiveModel) + } + "\tassert.PanicsWithValue(t, $expectedPanicValueCode, $actualFunctionCallLambda)" + } else if (expectedPanicValueSourceGoType.implementsError) { + "\tassert.PanicsWithError(t, $expectedPanicValue, $actualFunctionCallLambda)" + } else { + "\tassert.Panics(t, $actualFunctionCallLambda)" + } + + return "$testFunctionSignatureDeclaration {\n$testFunctionBody\n}" + } +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/GoFuzzedValueUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/GoFuzzedValueUtil.kt new file mode 100644 index 0000000000..d4230de92e --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/GoFuzzedValueUtil.kt @@ -0,0 +1,6 @@ +package org.utbot.go.util + +import org.utbot.framework.plugin.api.go.GoUtModel +import org.utbot.fuzzer.FuzzedValue + +val FuzzedValue.goRequiredImports get() = (this.model as GoUtModel).requiredImports \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt new file mode 100644 index 0000000000..2d8a5666c0 --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/JsonUtil.kt @@ -0,0 +1,24 @@ +package org.utbot.go.util + +import com.beust.klaxon.Klaxon +import java.io.File + +fun writeJsonToFileOrFail(targetObject: T, jsonFile: File) { + val targetObjectAsJson = Klaxon().toJsonString(targetObject) + jsonFile.writeText(targetObjectAsJson) +} + +inline fun parseFromJsonOrFail(jsonFile: File): T { + val result = Klaxon().parse(jsonFile) + if (result == null) { + val rawResults = try { + jsonFile.readText() + } catch (exception: Exception) { + null + } + throw RuntimeException( + "Failed to deserialize results: $rawResults" + ) + } + return result +} \ No newline at end of file diff --git a/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt new file mode 100644 index 0000000000..0e0151be0e --- /dev/null +++ b/utbot-go/src/main/kotlin/org/utbot/go/util/ProcessExecutionUtil.kt @@ -0,0 +1,27 @@ +package org.utbot.go.util + +import java.io.File +import java.io.InputStreamReader + +fun executeCommandByNewProcessOrFail(command: List, workingDirectory: File, executionTargetName: String) { + val executedProcess = runCatching { + val process = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + .start() + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + "Execution of $executionTargetName in child process failed with throwable: $it" + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + "Execution of $executionTargetName in child process failed with non-zero exit code = $exitCode:\n$processOutput" + ) + } +} \ No newline at end of file diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go new file mode 100644 index 0000000000..427cc7a9e7 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_results.go @@ -0,0 +1,32 @@ +package main + +import "go/token" + +type AnalyzedType struct { + Name string `json:"name"` + ImplementsError bool `json:"implementsError"` +} + +type AnalyzedFunctionParameter struct { + Name string `json:"name"` + Type AnalyzedType `json:"type"` +} + +type AnalyzedFunction struct { + Name string `json:"name"` + Parameters []AnalyzedFunctionParameter `json:"parameters"` + ResultTypes []AnalyzedType `json:"resultTypes"` + position token.Pos +} + +type AnalysisResult struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + PackageName string `json:"packageName"` + AnalyzedFunctions []AnalyzedFunction `json:"analyzedFunctions"` + NotSupportedFunctionsNames []string `json:"notSupportedFunctionsNames"` + NotFoundFunctionsNames []string `json:"notFoundFunctionsNames"` +} + +type AnalysisResults struct { + Results []AnalysisResult `json:"results"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go new file mode 100644 index 0000000000..b925cc8ff5 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analysis_targets.go @@ -0,0 +1,10 @@ +package main + +type AnalysisTarget struct { + AbsoluteFilePath string `json:"absoluteFilePath"` + TargetFunctionsNames []string `json:"targetFunctionsNames"` +} + +type AnalysisTargets struct { + Targets []AnalysisTarget `json:"targets"` +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go new file mode 100644 index 0000000000..cc4a18510e --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/analyzer_core.go @@ -0,0 +1,143 @@ +package main + +import ( + "go/types" + "sort" +) + +func implementsError(typ types.Type) bool { + // TODO: get types.Interface of "error", for now straightforward strings equals + //if(types.Implements(typ.Underlying(), ErrorInterface)) { + // return true + //} + return typ.Underlying().String() == "interface{Error() string}" +} + +func toAnalyzedType(typ types.Type) AnalyzedType { + implementsError := implementsError(typ) + var name string + if implementsError { + name = "error" + } else { + name = typ.Underlying().String() + } + return AnalyzedType{ + Name: name, + ImplementsError: implementsError, + } +} + +// for now supports only basic and error result types +func checkTypeIsSupported(typ types.Type, isResultType bool) bool { + underlyingType := typ.Underlying() // analyze real type, not alias or defined type + if _, ok := underlyingType.(*types.Basic); ok { + return true + } + if isResultType && implementsError(underlyingType) { + return true + } + return false +} + +func checkIsSupported(signature *types.Signature) bool { + if signature.Recv() != nil { // is method + return false + } + if signature.TypeParams() != nil { // has type params + return false + } + if signature.Variadic() { // is variadic + return false + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + result := results.At(i) + if !checkTypeIsSupported(result.Type(), true) { + return false + } + } + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + parameter := parameters.At(i) + if !checkTypeIsSupported(parameter.Type(), false) { + return false + } + } + } + return true +} + +//goland:noinspection GoPreferNilSlice +func collectTargetAnalyzedFunctions(info *types.Info, targetFunctionsNames []string) ( + analyzedFunctions []AnalyzedFunction, + notSupportedFunctionsNames []string, + notFoundFunctionsNames []string, +) { + analyzedFunctions = []AnalyzedFunction{} + notSupportedFunctionsNames = []string{} + notFoundFunctionsNames = []string{} + + selectAll := len(targetFunctionsNames) == 0 + foundTargetFunctionsNamesMap := map[string]bool{} + for _, functionName := range targetFunctionsNames { + foundTargetFunctionsNamesMap[functionName] = false + } + + for _, obj := range info.Defs { + switch typedObj := obj.(type) { + case *types.Func: + analyzedFunction := AnalyzedFunction{ + Name: typedObj.Name(), + Parameters: []AnalyzedFunctionParameter{}, + ResultTypes: []AnalyzedType{}, + position: typedObj.Pos(), + } + + if !selectAll { + if isFound, ok := foundTargetFunctionsNamesMap[analyzedFunction.Name]; !ok || isFound { + continue + } else { + foundTargetFunctionsNamesMap[analyzedFunction.Name] = true + } + } + + signature := typedObj.Type().(*types.Signature) + if !checkIsSupported(signature) { + notSupportedFunctionsNames = append(notSupportedFunctionsNames, analyzedFunction.Name) + continue + } + if parameters := signature.Params(); parameters != nil { + for i := 0; i < parameters.Len(); i++ { + parameter := parameters.At(i) + analyzedFunction.Parameters = append(analyzedFunction.Parameters, + AnalyzedFunctionParameter{ + Name: parameter.Name(), + Type: toAnalyzedType(parameter.Type()), + }) + } + } + if results := signature.Results(); results != nil { + for i := 0; i < results.Len(); i++ { + result := results.At(i) + analyzedFunction.ResultTypes = append(analyzedFunction.ResultTypes, toAnalyzedType(result.Type())) + } + } + + analyzedFunctions = append(analyzedFunctions, analyzedFunction) + } + } + + for functionName, isFound := range foundTargetFunctionsNamesMap { + if !isFound { + notFoundFunctionsNames = append(notFoundFunctionsNames, functionName) + } + } + sort.Slice(analyzedFunctions, func(i, j int) bool { + return analyzedFunctions[i].position < analyzedFunctions[j].position + }) + sort.Sort(sort.StringSlice(notSupportedFunctionsNames)) + sort.Sort(sort.StringSlice(notFoundFunctionsNames)) + + return analyzedFunctions, notSupportedFunctionsNames, notFoundFunctionsNames +} diff --git a/utbot-go/src/main/resources/go_source_code_analyzer/main.go b/utbot-go/src/main/resources/go_source_code_analyzer/main.go new file mode 100644 index 0000000000..395ed7d953 --- /dev/null +++ b/utbot-go/src/main/resources/go_source_code_analyzer/main.go @@ -0,0 +1,77 @@ +package main + +import ( + "encoding/json" + "flag" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "log" + "os" +) + +func checkError(err error) { + if err != nil { + log.Fatal(err.Error()) + } +} + +func analyzeTarget(target AnalysisTarget) AnalysisResult { + // first of all, parse AST + fset := token.NewFileSet() + fileAst, astErr := parser.ParseFile(fset, target.AbsoluteFilePath, nil, 0) + checkError(astErr) + + // collect info about types + typesConfig := types.Config{Importer: importer.Default()} + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Types: make(map[ast.Expr]types.TypeAndValue), + } + _, typesCheckErr := typesConfig.Check(target.AbsoluteFilePath, fset, []*ast.File{fileAst}, info) + checkError(typesCheckErr) + + // collect required info about selected functions + analyzedFunctions, notSupportedFunctionsNames, notFoundFunctionsNames := + collectTargetAnalyzedFunctions(info, target.TargetFunctionsNames) + + return AnalysisResult{ + AbsoluteFilePath: target.AbsoluteFilePath, + PackageName: fileAst.Name.String(), + AnalyzedFunctions: analyzedFunctions, + NotSupportedFunctionsNames: notSupportedFunctionsNames, + NotFoundFunctionsNames: notFoundFunctionsNames, + } +} + +func main() { + var targetsFilePath, resultsFilePath string + flag.StringVar(&targetsFilePath, "targets", "", "path to JSON file to read analysis targets from") + flag.StringVar(&resultsFilePath, "results", "", "path to JSON file to write analysis results to") + flag.Parse() + + // read and deserialize targets + targetsBytes, readErr := os.ReadFile(targetsFilePath) + checkError(readErr) + + var analysisTargets AnalysisTargets + fromJsonErr := json.Unmarshal(targetsBytes, &analysisTargets) + checkError(fromJsonErr) + + // parse each requested Go source file + analysisResults := AnalysisResults{Results: []AnalysisResult{}} + for _, target := range analysisTargets.Targets { + result := analyzeTarget(target) + analysisResults.Results = append(analysisResults.Results, result) + } + + // serialize and write results + jsonBytes, toJsonErr := json.MarshalIndent(analysisResults, "", " ") + checkError(toJsonErr) + + writeErr := os.WriteFile(resultsFilePath, jsonBytes, os.ModePerm) + checkError(writeErr) +} diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts new file mode 100644 index 0000000000..12bbb4ecf9 --- /dev/null +++ b/utbot-intellij-js/build.gradle.kts @@ -0,0 +1,74 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation(project(":utbot-ui-commons")) + + //Family + implementation(project(":utbot-js")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = listOf( + "java", + "org.jetbrains.kotlin:212-1.7.10-release-333-IJ5457.46" + ) + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + plugins.set( + when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PU" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + } + ) + + version.set("212.5712.43") + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt new file mode 100644 index 0000000000..57fae37b78 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogProcessor.kt @@ -0,0 +1,198 @@ +package org.utbot.intellij.plugin.language.js + +import api.JsTestGenerator +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.Task +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.file.PsiDirectoryFactory +import com.intellij.util.concurrency.AppExecutorUtil +import org.jetbrains.kotlin.idea.util.application.invokeLater +import org.jetbrains.kotlin.idea.util.application.runReadAction +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.konan.file.File +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.utils.testModules +import settings.JsExportsSettings.endComment +import settings.JsExportsSettings.exportsLinePrefix +import settings.JsExportsSettings.startComment +import settings.JsTestGenerationSettings.dummyClassName + +object JsDialogProcessor { + + fun createDialogAndGenerateTests( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + containingFilePath: String, + editor: Editor, + ) { + createDialog(project, srcModule, fileMethods, focusedMethod, containingFilePath)?.let { dialogProcessor -> + if (!dialogProcessor.showAndGet()) return + /* + Since Tern.js accesses containing file, sync with file system required before test generation. + */ + runWriteAction { + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + createTests(dialogProcessor.model, containingFilePath, editor) + } + } + + private fun createDialog( + project: Project, + srcModule: Module, + fileMethods: Set, + focusedMethod: JSMemberInfo?, + filePath: String, + ): JsDialogWindow? { + val testModules = srcModule.testModules(project) + + if (testModules.isEmpty()) { + val errorMessage = """ + No test source roots found in the project.
+ Please,
create or configure at least one test source root. + """.trimIndent() + showErrorDialogLater(project, errorMessage, "Test source roots not found") + return null + } + + return JsDialogWindow( + JsTestsModel( + project = project, + srcModule = srcModule, + potentialTestModules = testModules, + fileMethods = fileMethods, + selectedMethods = if (focusedMethod != null) setOf(focusedMethod) else emptySet(), + ).apply { + containingFilePath = filePath + } + ) + } + + private fun unblockDocument(project: Project, document: Document) { + PsiDocumentManager.getInstance(project).apply { + commitDocument(document) + doPostponedOperationsAndUnblockDocument(document) + } + } + + private fun createTests(model: JsTestsModel, containingFilePath: String, editor: Editor) { + val normalizedContainingFilePath = containingFilePath.replace("/", "\\") + (object : Task.Backgroundable(model.project, "Generate tests") { + override fun run(indicator: ProgressIndicator) { + indicator.isIndeterminate = false + indicator.text = "Generate tests: read classes" + val testDir = PsiDirectoryFactory.getInstance(project).createDirectory( + model.testSourceRoot!! + ) + val testFileName = normalizedContainingFilePath.substringAfterLast(File.separator) + .replace(Regex(".js"), "Test.js") + val testGenerator = JsTestGenerator( + fileText = editor.document.text, + sourceFilePath = normalizedContainingFilePath, + projectPath = model.project.basePath?.replace("/", "\\") + ?: throw IllegalStateException("Can't access project path."), + selectedMethods = runReadAction { + model.selectedMethods.map { + it.member.name!! + } + }, + parentClassName = runReadAction { + val name = (model.selectedMethods.first().member.parent as ES6Class).name + if (name == dummyClassName) null else name + }, + outputFilePath = "${testDir.virtualFile.path}/$testFileName".replace("/", "\\"), + exportsManager = partialApplication(JsDialogProcessor::manageExports, editor, project), + timeout = model.timeout, + ) + + indicator.fraction = indicator.fraction.coerceAtLeast(0.9) + indicator.text = "Generate code for tests" + + val generatedCode = testGenerator.run() + invokeLater { + runWriteAction { + val testPsiFile = + testDir.findFile(testFileName) ?: PsiFileFactory.getInstance(project) + .createFileFromText(testFileName, JsLanguageAssistant.jsLanguage, generatedCode) + val testFileEditor = + CodeInsightUtil.positionCursor(project, testPsiFile, testPsiFile) + unblockDocument(project, testFileEditor.document) + testFileEditor.document.setText(generatedCode) + unblockDocument(project, testFileEditor.document) + testDir.findFile(testFileName) ?: testDir.add(testPsiFile) + } + } + } + }).queue() + } + + private fun partialApplication(f: (A, B, C) -> Unit, a: A, b: B): (C) -> Unit { + return { c: C -> f(a, b, c) } + } + + private fun manageExports(editor: Editor, project: Project, exports: List) { + AppExecutorUtil.getAppExecutorService().submit { + invokeLater { + val exportLine = exports.joinToString(", ") + val fileText = editor.document.text + when { + fileText.contains("$exportsLinePrefix{$exportLine}") -> {} + + fileText.contains(startComment) && !fileText.contains("$exportsLinePrefix{$exportLine}") -> { + val regex = Regex("\n$startComment\n(.*)\n$endComment") + regex.find(fileText)?.groups?.get(1)?.value?.let { + val exportsRegex = Regex("\\{(.*)}") + val existingExportsLine = exportsRegex.find(it)!!.groupValues[1] + val existingExportsSet = + existingExportsLine.filterNot { c -> c == ' ' }.split(',').toMutableSet() + existingExportsSet.addAll(exports) + val resLine = existingExportsSet.joinToString() + val swappedText = fileText.replace(it, "$exportsLinePrefix{$resLine}") + runWriteAction { + with(editor.document) { + unblockDocument(project, this) + setText(swappedText) + unblockDocument(project, this) + } + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } + } + + else -> { + val line = buildString { + append("\n$startComment") + append("\n$exportsLinePrefix{$exportLine}") + append("\n$endComment") + } + runWriteAction { + with(editor.document) { + unblockDocument(project, this) + setText(fileText + line) + unblockDocument(project, this) + } + with(FileDocumentManager.getInstance()) { + saveDocument(editor.document) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt new file mode 100644 index 0000000000..e3cf4ca4dc --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsDialogWindow.kt @@ -0,0 +1,299 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.lang.javascript.refactoring.ui.JSMemberSelectionTable +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.Computable +import com.intellij.openapi.vfs.StandardFileSystems +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import com.intellij.psi.PsiManager +import com.intellij.refactoring.PackageWrapper +import com.intellij.refactoring.ui.PackageNameReferenceEditorCombo +import com.intellij.refactoring.util.RefactoringUtil +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.CheckBox +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.Panel +import com.intellij.ui.layout.Cell +import com.intellij.ui.layout.panel +import com.intellij.util.IncorrectOperationException +import com.intellij.util.ui.JBUI +import framework.codegen.JsCodeLanguage +import framework.codegen.Mocha +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton +import org.utbot.intellij.plugin.ui.utils.addSourceRootIfAbsent +import org.utbot.intellij.plugin.ui.utils.testRootType +import settings.JsTestGenerationSettings.defaultTimeout +import utils.JsCmdExec +import java.awt.BorderLayout +import java.util.Locale +import javax.swing.DefaultComboBoxModel +import javax.swing.JComboBox +import javax.swing.JComponent +import kotlin.concurrent.thread + +class JsDialogWindow(val model: JsTestsModel) : DialogWrapper(model.project) { + + private val items = model.fileMethods + + private val functionsTable = JSMemberSelectionTable(items, null, null).apply { + val height = this.rowHeight * (items.size.coerceAtMost(12) + 1) + this.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private fun findTestPackageComboValue() = SAME_PACKAGE_LABEL + + private val cbSpecifyTestPackage = CheckBox("Specify destination package", false) + private val testPackageField = PackageNameReferenceEditorCombo( + findTestPackageComboValue(), + model.project, + RECENTS_KEY, + "Choose Destination Package" + ) + + private val testSourceFolderField = TestFolderComboWithBrowseButton(model) + private val testFrameworks = ComboBox(DefaultComboBoxModel(arrayOf(Mocha))) + + private var initTestFrameworkPresenceThread: Thread + + private lateinit var panel: DialogPanel + + private val timeoutSpinner = + JBIntSpinner( + defaultTimeout.toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + + init { + title = "Generate Tests with UtBot" + initTestFrameworkPresenceThread = thread(start = true) { + JsCodeLanguage.testFrameworks.forEach { + it.isInstalled = findFrameworkLibrary(it.displayName.lowercase(Locale.getDefault())) + } + } + isResizable = false + init() + } + + + @Suppress("UNCHECKED_CAST") + override fun createCenterPanel(): JComponent { + panel = panel { + row("Test source root:") { + component(testSourceFolderField) + } + row("Test framework:") { + component( + Panel().apply { + add(testFrameworks as ComboBox, BorderLayout.LINE_START) + } + ) + } + row("Timeout for Node.js (in seconds):") { + panelWithHelpTooltip("The execution timeout") { + component(timeoutSpinner) + component(JBLabel("sec")) + } + } + row { + component(cbSpecifyTestPackage) + }.apply { visible = false } + row("Destination package:") { + component(testPackageField) + }.apply { visible = false } + row("Generate test methods for:") {} + row { + scrollPane(functionsTable) + } + } + updateMembersTable() + setListeners() + return panel + } + + + private inline fun Cell.panelWithHelpTooltip(tooltipText: String?, crossinline init: Cell.() -> Unit): Cell { + init() + tooltipText?.let { component(ContextHelpLabel.create(it)) } + return this + } + + + override fun doOKAction() { + model.testPackageName = + if (testPackageField.text != SAME_PACKAGE_LABEL) testPackageField.text else "" + val selected = functionsTable.selectedMemberInfos.toSet() + model.selectedMethods = if (selected.any()) selected else emptySet() + model.testFramework = testFrameworks.item + model.timeout = timeoutSpinner.number.toLong() + + configureTestFrameworkIfRequired() + try { + val testRootPrepared = createTestRootAndPackages() + if (!testRootPrepared) { + showTestRootAbsenceErrorMessage() + return + } + } catch (e: IncorrectOperationException) { + println(e.message) + + } + super.doOKAction() + } + + private fun updateMembersTable() { + if (items.isEmpty()) isOKActionEnabled = false + val focusedNames = model.selectedMethods.map { it.member.name } + val selectedMethods = items.filter { + focusedNames.contains(it.member.name) + } + if (selectedMethods.isEmpty()) { + checkMembers(items) + } else { + checkMembers(selectedMethods) + } + } + + private fun showTestRootAbsenceErrorMessage() = + Messages.showErrorDialog( + "Test source root is not configured or is located out of content entry!", + "Generation Error" + ) + + private fun configureTestFrameworkIfRequired() { + initTestFrameworkPresenceThread.join() + val frameworkNotInstalled = !testFrameworks.item.isInstalled + if (frameworkNotInstalled) { + Messages.showErrorDialog( + "Test framework ${testFrameworks.item.displayName} is not installed. " + + "Run \"npm i -g ${testFrameworks.item.displayName}\".", + "Missing Framework" + ) + } + } + + private fun findFrameworkLibrary(npmPackageName: String): Boolean { + val (bufferedReader, _) = JsCmdExec.runCommand( + cmd = "npm list -g", + dir = model.project.basePath!!, + shouldWait = true, + timeout = 10, + ) + val checkForPackageText = bufferedReader.readText() + bufferedReader.close() + if (checkForPackageText == "") { + Messages.showErrorDialog( + model.project, + "Node.js is not installed", + title, + ) + return false + } + return checkForPackageText.contains(npmPackageName) + } + + private fun setListeners() { + + testSourceFolderField.childComponent.addActionListener { event -> + with((event.source as JComboBox<*>).selectedItem) { + if (this is VirtualFile) { + model.setSourceRootAndFindTestModule(this@with) + } else { + model.setSourceRootAndFindTestModule(null) + } + } + } + } + + private fun getOrCreateTestRoot(testSourceRoot: VirtualFile): Boolean { + val modifiableModel = ModuleRootManager.getInstance(model.testModule).modifiableModel + try { + val contentEntry = modifiableModel.contentEntries + .filterNot { it.file == null } + .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, false) } + ?: return false + + contentEntry.addSourceRootIfAbsent( + modifiableModel, + testSourceRoot.url, + CodegenLanguage.JS.testRootType() + ) + return true + } finally { + if (modifiableModel.isWritable && !modifiableModel.isDisposed) modifiableModel.dispose() + } + } + + private fun createTestRootAndPackages(): Boolean { + model.setSourceRootAndFindTestModule(createDirectoryIfMissing(model.testSourceRoot)) + val testSourceRoot = model.testSourceRoot ?: return false + + if (model.testSourceRoot?.isDirectory != true) return false + if (getOrCreateTestRoot(testSourceRoot)) { + if (cbSpecifyTestPackage.isSelected) { + createSelectedPackage(testSourceRoot) + } else { + createPackagesByClasses(testSourceRoot) + } + return true + } + return false + } + + private fun createPackageWrapper(packageName: String?): PackageWrapper = + PackageWrapper(PsiManager.getInstance(model.project), trimPackageName(packageName)) + + private fun trimPackageName(name: String?): String = name?.trim() ?: "" + + private fun createPackagesByClasses(testSourceRoot: VirtualFile) { + val packageNames = model.srcClasses.map { it.packageName }.sortedBy { it.length } + for (packageName in packageNames) { + runWriteAction { + RefactoringUtil.createPackageDirectoryInSourceRoot(createPackageWrapper(packageName), testSourceRoot) + } + } + } + + private fun createSelectedPackage(testSourceRoot: VirtualFile) = + runWriteAction { + RefactoringUtil.createPackageDirectoryInSourceRoot( + createPackageWrapper(testPackageField.text), + testSourceRoot + ) + } + + private fun createDirectoryIfMissing(dir: VirtualFile?): VirtualFile? { + val file = if (dir is FakeVirtualFile) { + WriteCommandAction.runWriteCommandAction(model.project, Computable { + VfsUtil.createDirectoryIfMissing(dir.path) + }) + } else { + dir + } ?: return null + return if (VfsUtil.virtualToIoFile(file).isFile) { + null + } else { + StandardFileSystems.local().findFileByPath(file.path) + } + } + + + private fun checkMembers(members: Collection) = members.forEach { it.isChecked = true } +} + +private const val RECENTS_KEY = "org.utbot.recents" +private const val SAME_PACKAGE_LABEL = "same as for sources" +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt new file mode 100644 index 0000000000..4a510b34e7 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsLanguageAssistant.kt @@ -0,0 +1,142 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.lang.Language +import com.intellij.lang.ecmascript6.psi.ES6Class +import com.intellij.lang.javascript.psi.JSFile +import com.intellij.lang.javascript.psi.JSFunction +import com.intellij.lang.javascript.psi.ecmal4.JSClass +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.util.PsiTreeUtil +import org.jetbrains.kotlin.idea.util.projectStructure.module +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import settings.JsTestGenerationSettings.dummyClassName + +object JsLanguageAssistant : LanguageAssistant() { + + private const val jsId = "ECMAScript 6" + val jsLanguage: Language = Language.findLanguageByID(jsId) ?: error("JavaScript language wasn't found") + + private data class PsiTargets( + val methods: Set, + val focusedMethod: JSMemberInfo?, + val module: Module, + val containingFilePath: String, + val editor: Editor, + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (methods, focusedMethod, module, containingFilePath, editor) = getPsiTargets(e) ?: return + JsDialogProcessor.createDialogAndGenerateTests( + project = project, + srcModule = module, + fileMethods = methods, + focusedMethod = focusedMethod, + containingFilePath = containingFilePath, + editor = editor, + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): PsiTargets? { + e.project ?: return null + val virtualFile = (e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return null).path + val editor = e.getData(CommonDataKeys.EDITOR) ?: return null + val file = e.getData(CommonDataKeys.PSI_FILE) as? JSFile ?: return null + val element = findPsiElement(file, editor) ?: return null + val module = element.module ?: return null + val focusedMethod = getContainingMethod(element) + containingClass(element)?.let { + val methods = it.functions + val memberInfos = generateMemberInfo(e.project!!, methods.toList(), it) + val focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + ) + } + val memberInfos = generateMemberInfo(e.project!!, file.statements.filterIsInstance()) + val focusedMethodMI = memberInfos.find { member -> + member.member?.name == focusedMethod?.name + } + return PsiTargets( + methods = memberInfos, + focusedMethod = focusedMethodMI, + module = module, + containingFilePath = virtualFile, + editor = editor, + ) + } + + private fun getContainingMethod(element: PsiElement): JSFunction? { + if (element is JSFunction) + return element + + val parent = element.parent ?: return null + return getContainingMethod(parent) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + return element + } + + private fun containingClass(element: PsiElement) = + PsiTreeUtil.getParentOfType(element, ES6Class::class.java, false) + + private fun buildClassStringFromMethods(methods: List): String { + var strBuilder = "\n" + val filteredMethods = methods.filterNot { method -> method.name == "constructor" } + filteredMethods.forEach { + strBuilder += it.text.replace("function ", "") + } + // Creating a class with a random name. It won't affect user's code since it is created in abstract PsiFile. + return "class $dummyClassName {$strBuilder}" + } + + /* + Small hack: generating a string source code of an "impossible" class in order to + generate a PsiFile with it, then extract ES6Class from it, then extract MemberInfos. + Created for top-level functions that don't have a parent class. + */ + private fun generateMemberInfo( + project: Project, + methods: List, + jsClass: JSClass? = null + ): Set { + jsClass?.let { + val res = mutableListOf() + JSMemberInfo.extractClassMembers(it, res) { member -> + member is JSFunction + } + return res.toSet() + } + val strClazz = buildClassStringFromMethods(methods) + val abstractPsiFile = PsiFileFactory.getInstance(project) + .createFileFromText(jsLanguage, strClazz) + val clazz = PsiTreeUtil.getChildOfType(abstractPsiFile, JSClass::class.java) + val res = mutableListOf() + JSMemberInfo.extractClassMembers(clazz!!, res) { true } + return res.toSet() + } +} \ No newline at end of file diff --git a/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt new file mode 100644 index 0000000000..e4efd0a722 --- /dev/null +++ b/utbot-intellij-js/src/main/kotlin/org/utbot/intellij/plugin/language/js/JsTestsModel.kt @@ -0,0 +1,29 @@ +package org.utbot.intellij.plugin.language.js + +import com.intellij.lang.javascript.refactoring.util.JSMemberInfo +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.psi.PsiClass +import org.jetbrains.kotlin.idea.core.getPackage +import org.utbot.framework.codegen.TestFramework +import org.utbot.intellij.plugin.models.BaseTestsModel +import settings.JsTestGenerationSettings.defaultTimeout + + +val PsiClass.packageName: String get() = this.containingFile.containingDirectory.getPackage()?.qualifiedName ?: "" + +class JsTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + val fileMethods: Set, + var selectedMethods: Set, +) : BaseTestsModel( + project, srcModule, potentialTestModules, emptySet() +) { + + var timeout = defaultTimeout + + lateinit var testFramework: TestFramework + lateinit var containingFilePath: String +} \ No newline at end of file diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts new file mode 100644 index 0000000000..7e69443526 --- /dev/null +++ b/utbot-intellij-python/build.gradle.kts @@ -0,0 +1,72 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation(project(":utbot-ui-commons")) + + //Family + implementation(project(":utbot-python")) +} + +intellij { + + val androidPlugins = listOf("org.jetbrains.android") + + val jvmPlugins = listOf( + "java", + "org.jetbrains.kotlin:212-1.7.10-release-333-IJ5457.46" + ) + + val pythonCommunityPlugins = listOf( + "PythonCore:${pythonCommunityPluginVersion}" + ) + + val pythonUltimatePlugins = listOf( + "Pythonid:${pythonUltimatePluginVersion}" + ) + + val jsPlugins = listOf( + "JavaScript" + ) + + plugins.set(when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + }) + + version.set("212.5712.43") + type.set(ideType) +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IterationUtil.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IterationUtil.kt new file mode 100644 index 0000000000..c322ac2c88 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/IterationUtil.kt @@ -0,0 +1,13 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.psi.PsiElement + +object IterationUtils { + inline fun getContainingElement(element: PsiElement): T? { + var result = element + while (result !is T && (result.parent != null)) { + result = result.parent + } + return result as? T + } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt new file mode 100644 index 0000000000..2710143b96 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt @@ -0,0 +1,266 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.codeInsight.CodeInsightUtil +import com.intellij.openapi.application.invokeAndWaitIfNeeded +import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.editor.Document +import com.intellij.openapi.fileEditor.OpenFileDescriptor +import com.intellij.openapi.module.Module +import com.intellij.openapi.progress.ProgressIndicator +import com.intellij.openapi.progress.ProgressManager +import com.intellij.openapi.progress.Task.Backgroundable +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.vfs.LocalFileSystem +import com.intellij.openapi.vfs.VfsUtil +import com.intellij.openapi.vfs.VfsUtilCore +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.PsiFileFactory +import com.intellij.psi.impl.file.PsiDirectoryFactory +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import com.jetbrains.python.psi.PyClass +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.util.application.runWriteAction +import org.jetbrains.kotlin.idea.util.module +import org.jetbrains.kotlin.idea.util.projectStructure.sdk +import org.utbot.common.PathUtil.toPath +import org.utbot.common.appendHtmlLine +import org.utbot.framework.UtSettings +import org.utbot.intellij.plugin.ui.utils.showErrorDialogLater +import org.utbot.intellij.plugin.ui.WarningTestsReportNotifier +import org.utbot.intellij.plugin.ui.utils.testModules +import org.utbot.python.code.PythonCode +import org.utbot.python.code.PythonCode.Companion.getFromString +import org.utbot.python.PythonMethod +import org.utbot.python.PythonTestGenerationProcessor +import org.utbot.python.utils.camelToSnakeCase +import org.utbot.python.PythonTestGenerationProcessor.processTestGeneration +import org.utbot.python.framework.codegen.PythonCodeLanguage +import org.utbot.python.utils.RequirementsUtils.installRequirements +import org.utbot.python.utils.RequirementsUtils.requirements +import java.io.File +import kotlin.io.path.Path + +const val DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS = 2000L + +object PythonDialogProcessor { + fun createDialogAndGenerateTests( + project: Project, + functionsToShow: Set, + containingClass: PyClass?, + focusedMethod: PyFunction?, + file: PyFile + ) { + val dialog = createDialog(project, functionsToShow, containingClass, focusedMethod, file) + if (!dialog.showAndGet()) { + return + } + + createTests(project, dialog.model) + } + + private fun createDialog( + project: Project, + functionsToShow: Set, + containingClass: PyClass?, + focusedMethod: PyFunction?, + file: PyFile + ): PythonDialogWindow { + val srcModule = findSrcModule(functionsToShow) + val testModules = srcModule.testModules(project) + val (directoriesForSysPath, moduleToImport) = getDirectoriesForSysPath(srcModule, file) + + return PythonDialogWindow( + PythonTestsModel( + project, + srcModule, + testModules, + functionsToShow, + containingClass, + if (focusedMethod != null) setOf(focusedMethod) else null, + file, + directoriesForSysPath, + moduleToImport, + UtSettings.utBotGenerationTimeoutInMillis, + DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS, + visitOnlySpecifiedSource = false, + codeGenLanguage = PythonCodeLanguage, + ) + ) + } + + private fun findSelectedPythonMethods(model: PythonTestsModel): List? { + val code = getPyCodeFromPyFile(model.file, model.currentPythonModule) ?: return null + + val shownFunctions: Set = + if (model.containingClass == null) { + code.getToplevelFunctions().toSet() + } else { + val classes = code.getToplevelClasses() + val myClass = classes.find { it.name == model.containingClass.name } + ?: error("Didn't find containing class") + myClass.methods.toSet() + } + + return model.selectedFunctions.map { pyFunction -> + shownFunctions.find { pythonMethod -> + pythonMethod.name == pyFunction.name + } ?: error("Didn't find PythonMethod ${pyFunction.name}") + } + } + + private fun getOutputFileName(model: PythonTestsModel) = + "test_${model.currentPythonModule.camelToSnakeCase().replace('.', '_')}.py" + + private fun createTests(project: Project, model: PythonTestsModel) { + ProgressManager.getInstance().run(object : Backgroundable(project, "Generate python tests") { + override fun run(indicator: ProgressIndicator) { + val pythonPath = model.srcModule.sdk?.homePath + if (pythonPath == null) { + showErrorDialogLater( + project, + message = "Couldn't find Python interpreter", + title = "Python test generation error" + ) + return + } + val methods = findSelectedPythonMethods(model) + if (methods == null) { + showErrorDialogLater( + project, + message = "Couldn't parse file. Maybe it contains syntax error?", + title = "Python test generation error" + ) + return + } + val testSourceRootPath = model.testSourceRoot!!.path + processTestGeneration( + pythonPath = pythonPath, + testSourceRoot = testSourceRootPath, + pythonFilePath = model.file.virtualFile.path, + pythonFileContent = getContentFromPyFile(model.file), + directoriesForSysPath = model.directoriesForSysPath, + currentPythonModule = model.currentPythonModule, + pythonMethods = methods, + containingClassName = model.containingClass?.name, + timeout = model.timeout, + testFramework = model.testFramework, + timeoutForRun = model.timeoutForRun, + visitOnlySpecifiedSource = model.visitOnlySpecifiedSource, + isCanceled = { indicator.isCanceled }, + checkingRequirementsAction = { indicator.text = "Checking requirements" }, + requirementsAreNotInstalledAction = { + askAndInstallRequirementsLater(model.project, pythonPath) + PythonTestGenerationProcessor.MissingRequirementsActionResult.NOT_INSTALLED + }, + startedLoadingPythonTypesAction = { indicator.text = "Loading information about Python types" }, + startedTestGenerationAction = { indicator.text = "Generating tests" }, + notGeneratedTestsAction = { + showErrorDialogLater( + project, + message = "Cannot create tests for the following functions: " + it.joinToString(), + title = "Python test generation error" + ) + }, + writeTestTextToFile = { generatedCode -> + invokeLater { + runWriteAction { + val testDirAsVirtualFile = + LocalFileSystem.getInstance().refreshAndFindFileByIoFile(File(testSourceRootPath)) + val testDir = PsiDirectoryFactory.getInstance(project).createDirectory( + testDirAsVirtualFile!! + ) + val testFileName = getOutputFileName(model) + val testPsiFile = PsiFileFactory.getInstance(project) + .createFileFromText(testFileName, PythonLanguageAssistant.language, generatedCode) + testDir.findFile(testPsiFile.name)?.delete() + testDir.add(testPsiFile) + val file = testDir.findFile(testPsiFile.name)!! + CodeInsightUtil.positionCursor(project, file, file) + } + } + }, + processMypyWarnings = { + val message = it.fold(StringBuilder()) { acc, line -> acc.appendHtmlLine(line) } + WarningTestsReportNotifier.notify(message.toString()) + }, + startedCleaningAction = { indicator.text = "Cleaning up..." }, + pythonRunRoot = Path(model.testSourceRoot!!.path) + ) + } + }) + } + + private fun askAndInstallRequirementsLater(project: Project, pythonPath: String) { + val message = """ + Some requirements are not installed. + Requirements:
+ ${requirements.joinToString("
")} +
+ Install them? + """.trimIndent() + invokeLater { + val result = Messages.showYesNoDialog( + project, + message, + "Requirements Error", + null, + null + ) + if (result == Messages.NO) + return@invokeLater + + ProgressManager.getInstance().run(object : Backgroundable(project, "Installing requirements") { + override fun run(indicator: ProgressIndicator) { + val installResult = installRequirements(pythonPath) + + if (installResult.exitValue != 0) { + showErrorDialogLater( + project, + "Requirements installing failed", + "Requirements error" + ) + } + } + }) + } + } +} + +fun findSrcModule(functions: Collection): Module { + val srcModules = functions.mapNotNull { it.module }.distinct() + return when (srcModules.size) { + 0 -> error("Module for source classes not found") + 1 -> srcModules.first() + else -> error("Can not generate tests for classes from different modules") + } +} + +fun getContentFromPyFile(file: PyFile) = file.viewProvider.contents.toString() + +fun getPyCodeFromPyFile(file: PyFile, pythonModule: String): PythonCode? { + val content = getContentFromPyFile(file) + return getFromString(content, file.virtualFile.path, pythonModule = pythonModule) +} + +fun getDirectoriesForSysPath( + srcModule: Module, + file: PyFile +): Pair, String> { + val sources = ModuleRootManager.getInstance(srcModule).getSourceRoots(false).toMutableList() + val ancestor = ProjectFileIndex.SERVICE.getInstance(file.project).getContentRootForFile(file.virtualFile) + if (ancestor != null && !sources.contains(ancestor)) + sources.add(ancestor) + + var importPath = ancestor?.let { VfsUtil.getParentDir(VfsUtilCore.getRelativeLocation(file.virtualFile, it)) } ?: "" + if (importPath != "") + importPath += "." + + return Pair( + sources.map { it.path }.toSet(), + "${importPath}${file.name}".removeSuffix(".py").toPath().joinToString(".") + ) +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt new file mode 100644 index 0000000000..0d6bfd336b --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt @@ -0,0 +1,163 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.DialogPanel +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.ContextHelpLabel +import com.intellij.ui.JBIntSpinner +import com.intellij.ui.components.Panel +import com.intellij.ui.layout.CellBuilder +import com.intellij.ui.layout.Row +import com.intellij.ui.layout.panel +import com.intellij.util.ui.JBUI +import com.jetbrains.python.psi.* +import com.jetbrains.python.refactoring.classes.PyMemberInfoStorage +import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo +import com.jetbrains.python.refactoring.classes.ui.PyMemberSelectionTable +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton +import java.awt.BorderLayout +import java.util.concurrent.TimeUnit +import javax.swing.* + + +private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 + +class PythonDialogWindow(val model: PythonTestsModel): DialogWrapper(model.project) { + + private val functionsTable = PyMemberSelectionTable(emptyList(), null, false) + private val testSourceFolderField = TestFolderComboWithBrowseButton(model) + private val timeoutSpinnerForTotalTimeout = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(UtSettings.utBotGenerationTimeoutInMillis).toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + private val timeoutSpinnerForOneRun = + JBIntSpinner( + TimeUnit.MILLISECONDS.toSeconds(DEFAULT_TIMEOUT_FOR_RUN_IN_MILLIS).toInt(), + MINIMUM_TIMEOUT_VALUE_IN_SECONDS, + Int.MAX_VALUE, + MINIMUM_TIMEOUT_VALUE_IN_SECONDS + ) + private val testFrameworks = ComboBox(DefaultComboBoxModel(model.codeGenLanguage.testFrameworks.toTypedArray())) + + private val visitOnlySpecifiedSource = JCheckBox("Visit only specified source") + + private lateinit var panel: DialogPanel + + @Suppress("UNCHECKED_CAST") + private val itemsToHelpTooltip = hashMapOf( + (testFrameworks as ComboBox) to ContextHelpLabel.create(""), + ) + + init { + title = "Generate tests with UtBot" + isResizable = false + init() + } + + @Suppress("UNCHECKED_CAST") + override fun createCenterPanel(): JComponent { + + panel = panel { + row("Test source root:") { + component(testSourceFolderField) + } + row("Test framework:") { + makePanelWithHelpTooltip( + testFrameworks as ComboBox, + itemsToHelpTooltip[testFrameworks] + ) + } + row("Generate test methods for:") {} + row { + scrollPane(functionsTable) + } + row("Timeout for all selected functions:") { + component(timeoutSpinnerForTotalTimeout) + } + row("Timeout for one function run:") { + component(timeoutSpinnerForOneRun) + } + row { + component(visitOnlySpecifiedSource) + } + } + + updateFunctionsTable() + return panel + } + + private fun globalPyFunctionsToPyMemberInfo( + project: Project, + functions: Collection + ): List> { + val generator = PyElementGenerator.getInstance(project) + val newClass = generator.createFromText( + LanguageLevel.getDefault(), + PyClass::class.java, + "class __FakeWrapperUtBotClass_ivtdjvrdkgbmpmsclaro__:\npass" + ) + functions.forEach { + newClass.add(it) + } + val storage = PyMemberInfoStorage(newClass) + return storage.getClassMemberInfos(newClass) + } + + private fun pyFunctionsToPyMemberInfo(project: Project, functions: Collection, containingClass: PyClass?): List> { + if (containingClass == null) { + return globalPyFunctionsToPyMemberInfo(project, functions) + } + return PyMemberInfoStorage(containingClass).getClassMemberInfos(containingClass).filter { it.member is PyFunction } + } + + private fun updateFunctionsTable() { + val items = pyFunctionsToPyMemberInfo(model.project, model.functionsToDisplay, model.containingClass) + updateMethodsTable(items) + val height = functionsTable.rowHeight * (items.size.coerceAtMost(12) + 1) + functionsTable.preferredScrollableViewportSize = JBUI.size(-1, height) + } + + private fun updateMethodsTable(allMethods: Collection>) { + val focusedNames = model.focusedMethod?.map { it.name } + val selectedMethods = allMethods.filter { + focusedNames?.contains(it.member.name) ?: false + } + + if (selectedMethods.isEmpty()) { + checkMembers(allMethods) + } else { + checkMembers(selectedMethods) + } + + functionsTable.setMemberInfos(allMethods) + } + + private fun checkMembers(members: Collection>) = members.forEach { it.isChecked = true } + + private fun Row.makePanelWithHelpTooltip( + mainComponent: JComponent, + contextHelpLabel: ContextHelpLabel? + ): CellBuilder = + component(Panel().apply { + add(mainComponent, BorderLayout.LINE_START) + contextHelpLabel?.let { add(it, BorderLayout.LINE_END) } + }) + + override fun doOKAction() { + val selectedMembers = functionsTable.selectedMemberInfos + model.selectedFunctions = selectedMembers.mapNotNull { it.member as? PyFunction }.toSet() + model.testFramework = testFrameworks.item + model.timeout = TimeUnit.SECONDS.toMillis(timeoutSpinnerForTotalTimeout.number.toLong()) + model.timeoutForRun = TimeUnit.SECONDS.toMillis(timeoutSpinnerForOneRun.number.toLong()) + model.visitOnlySpecifiedSource = visitOnlySpecifiedSource.isSelected + + super.doOKAction() + } +} diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt new file mode 100644 index 0000000000..8a65b48253 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonLanguageAssistant.kt @@ -0,0 +1,78 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiFile +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant + +object PythonLanguageAssistant : LanguageAssistant() { + + private const val pythonID = "Python" + val language: Language = Language.findLanguageByID(pythonID) ?: error("Language wasn't found") + + data class Targets( + val functions: Set, + val containingClass: PyClass?, + val focusedFunction: PyFunction?, + val file: PyFile + ) + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (functions, containingClass, focusedFunction, file) = getPsiTargets(e) ?: return + + PythonDialogProcessor.createDialogAndGenerateTests( + project, + functions, + containingClass, + focusedFunction, + file + ) + } + + override fun update(e: AnActionEvent) { + e.presentation.isEnabled = getPsiTargets(e) != null + } + + private fun getPsiTargets(e: AnActionEvent): Targets? { + val editor = e.getData(CommonDataKeys.EDITOR) ?: return null + val file = e.getData(CommonDataKeys.PSI_FILE) as? PyFile ?: return null + val element = findPsiElement(file, editor) ?: return null + + val containingFunction = IterationUtils.getContainingElement(element) + val containingClass = IterationUtils.getContainingElement(element) + + if (containingClass == null) { + val functions = file.topLevelFunctions + if (functions.isEmpty()) + return null + + val focusedFunction = if (functions.contains(containingFunction)) containingFunction else null + return Targets(functions.toSet(), null, focusedFunction, file) + } + + val functions = containingClass.methods + if (functions.isEmpty()) + return null + + val focusedFunction = if (functions.any { it.name == containingFunction?.name }) containingFunction else null + return Targets(functions.toSet(), containingClass, focusedFunction, file) + } + + // this method is copy-paste from GenerateTestsActions.kt + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } +} \ No newline at end of file diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt new file mode 100644 index 0000000000..f2c8ed10b8 --- /dev/null +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt @@ -0,0 +1,34 @@ +package org.utbot.intellij.plugin.language.python + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.jetbrains.python.psi.PyClass +import com.jetbrains.python.psi.PyFile +import com.jetbrains.python.psi.PyFunction +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.CodeGenLanguage +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.models.BaseTestsModel + +class PythonTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + val functionsToDisplay: Set, + val containingClass: PyClass?, + val focusedMethod: Set?, + val file: PyFile, + val directoriesForSysPath: Set, + val currentPythonModule: String, + var timeout: Long, + var timeoutForRun: Long, + var visitOnlySpecifiedSource: Boolean, + val codeGenLanguage: CodeGenLanguage, +): BaseTestsModel( + project, + srcModule, + potentialTestModules +) { + lateinit var testFramework: TestFramework + lateinit var selectedFunctions: Set +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt index f651d568e1..83f05c7221 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt @@ -70,6 +70,10 @@ import java.nio.file.Path import java.util.concurrent.CancellationException import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass +import kotlin.reflect.full.functions +import org.utbot.intellij.plugin.util.IntelliJApiHelper.Target.* +import org.utbot.intellij.plugin.util.IntelliJApiHelper.run object CodeGenerationController { private val logger = KotlinLogging.logger {} @@ -548,6 +552,7 @@ object CodeGenerationController { when (model.codegenLanguage) { CodegenLanguage.JAVA -> it !is KtUltraLightClass CodegenLanguage.KOTLIN -> it is KtUltraLightClass + else -> throw UnsupportedOperationException() } } }) @@ -563,6 +568,7 @@ object CodeGenerationController { when (model.codegenLanguage) { CodegenLanguage.JAVA -> JavaTemplateUtil.INTERNAL_CLASS_TEMPLATE_NAME CodegenLanguage.KOTLIN -> "Kotlin Class" + else -> throw UnsupportedOperationException() } ) runWriteAction { testDirectory.findFile(testClassName + model.codegenLanguage.extension)?.delete() } @@ -694,6 +700,7 @@ object CodeGenerationController { JavaCodeStyleManager.getInstance(project).shortenClassReferences(reformatRange) } CodegenLanguage.KOTLIN -> ShortenReferences.DEFAULT.process((testClass as KtUltraLightClass).kotlinOrigin.containingKtFile) + else -> throw UnsupportedOperationException() } } } @@ -763,6 +770,7 @@ object CodeGenerationController { } } is RegularImport -> { } + else -> { } } } } @@ -796,7 +804,7 @@ object CodeGenerationController { } } - private fun unblockDocument(project: Project, document: Document) { + fun unblockDocument(project: Project, document: Document) { PsiDocumentManager.getInstance(project).apply { commitDocument(document) doPostponedOperationsAndUnblockDocument(document) diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt new file mode 100644 index 0000000000..42a7f9a3fa --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/language/JavaLanguage.kt @@ -0,0 +1,164 @@ +package org.utbot.intellij.plugin.language + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.roots.ProjectFileIndex +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.* +import com.intellij.psi.util.PsiTreeUtil +import com.intellij.refactoring.util.classMembers.MemberInfo +import org.jetbrains.kotlin.idea.core.getPackage +import org.jetbrains.kotlin.idea.core.util.toPsiDirectory +import org.jetbrains.kotlin.idea.core.util.toPsiFile +import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant +import org.utbot.intellij.plugin.ui.utils.PsiElementHandler +import org.utbot.intellij.plugin.util.extractFirstLevelMembers +import java.util.* + +object JvmLanguageAssistant : LanguageAssistant(){ + + override fun update(e: AnActionEvent) { + if (e.place == ActionPlaces.POPUP) { + e.presentation.text = "Tests with UnitTestBot..." + } + e.presentation.isEnabled = getPsiTargets(e) != null + } + + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val (srcClasses, focusedMethod, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return + UtTestsDialogProcessor.createDialogAndGenerateTests(project, srcClasses, extractMembersFromSrcClasses, focusedMethod) + } + + private fun getPsiTargets(e: AnActionEvent): Triple, MemberInfo?, Boolean>? { + val project = e.project ?: return null + val editor = e.getData(CommonDataKeys.EDITOR) + if (editor != null) { + //The action is being called from editor + val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null + val element = findPsiElement(file, editor) ?: return null + + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + val srcClass = psiElementHandler.containingClass(element) ?: return null + if (srcClass.isInterface) return null + val srcSourceRoot = srcClass.getSourceRoot() ?: return null + val srcMembers = srcClass.extractFirstLevelMembers(false) + val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) + + val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null + val matchingRoot = ModuleRootManager.getInstance(module).contentEntries + .flatMap { entry -> entry.sourceFolders.toList() } + .singleOrNull { folder -> folder.file == srcSourceRoot } + if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { + return null + } + + return Triple(setOf(srcClass), focusedMethod, true) + } + } else { + // The action is being called from 'Project' tool window + val srcClasses = mutableSetOf() + var selectedMethod: MemberInfo? = null + var extractMembersFromSrcClasses = false + val element = e.getData(CommonDataKeys.PSI_ELEMENT) ?: return null + if (element is PsiFileSystemItem) { + e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { + srcClasses += getAllClasses(project, it) + } + } else { + val file = element.containingFile ?: return null + val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) + + if (psiElementHandler.isCreateTestActionAvailable(element)) { + psiElementHandler.containingClass(element)?.let { + srcClasses += setOf(it) + extractMembersFromSrcClasses = true + if (it.extractFirstLevelMembers(false).isEmpty()) + return null + } + + if (element is PsiMethod) { + selectedMethod = MemberInfo(element) + } + } + } + srcClasses.removeIf { it.isInterface } + var commonSourceRoot = null as VirtualFile? + for (srcClass in srcClasses) { + if (commonSourceRoot == null) { + commonSourceRoot = srcClass.getSourceRoot()?: return null + } else if (commonSourceRoot != srcClass.getSourceRoot()) return null + } + if (commonSourceRoot == null) return null + val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null + + if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) + .flatMap { entry -> Arrays.stream(entry.sourceFolders) } + .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} + .findAny().isPresent ) return null + + return Triple(srcClasses.toSet(), selectedMethod, extractMembersFromSrcClasses) + } + return null + } + + private fun PsiElement?.getSourceRoot() : VirtualFile? { + val project = this?.project?: return null + val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null + return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) + } + + private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { + val offset = editor.caretModel.offset + var element = file.findElementAt(offset) + if (element == null && offset == file.textLength) { + element = file.findElementAt(offset - 1) + } + + return element + } + + private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { + // getParentOfType might return element which does not correspond to the standard Psi hierarchy. + // Thus, make transition to the Psi if it is required. + val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) + ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } + + return methods.singleOrNull { it.member == currentMethod } + } + + private fun getAllClasses(directory: PsiDirectory): Set { + val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() + for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) + return allClasses + } + + private fun getAllClasses(project: Project, virtualFiles: Array): Set { + val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } + val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } + val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } + + if (!dirsArePackages) { + return emptySet() + } + val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() + for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) + + return allClasses + } + + private fun getClassesFromFile(psiFile: PsiFile): List { + val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) + return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) + .map { psiElementHandler.toPsi(it, PsiClass::class.java) } + } +} \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index 99495e4079..5999f40154 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -11,7 +11,6 @@ import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi import com.intellij.openapi.module.Module -import com.intellij.openapi.module.ModuleUtil import com.intellij.openapi.project.Project import com.intellij.openapi.projectRoots.JavaSdkVersion import com.intellij.openapi.vfs.VirtualFile @@ -25,22 +24,22 @@ import org.utbot.framework.util.ConflictTriggers import org.utbot.intellij.plugin.settings.Settings import org.utbot.intellij.plugin.ui.utils.jdkVersion -data class GenerateTestsModel( - val project: Project, - val srcModule: Module, - val potentialTestModules: List, - var srcClasses: Set, +class GenerateTestsModel( + project: Project, + srcModule: Module, + potentialTestModules: List, + srcClasses: Set, val extractMembersFromSrcClasses: Boolean, var selectedMembers: Set, var timeout: Long, var generateWarningsForStaticMocking: Boolean = false, var fuzzingValue: Double = 0.05 +): BaseTestsModel( + project, + srcModule, + potentialTestModules, + srcClasses ) { - // GenerateTestsModel is supposed to be created with non-empty list of potentialTestModules. - // Otherwise, the error window is supposed to be shown earlier. - var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") - - var testSourceRoot: VirtualFile? = null fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { requireNotNull(newTestSourceRoot) @@ -57,7 +56,7 @@ data class GenerateTestsModel( ?: error("Could not find module for $newTestSourceRoot") } - val codegenLanguage = project.service().codegenLanguage +// val codegenLanguage = project.service().codegenLanguage var testPackageName: String? = null lateinit var testFramework: TestFramework @@ -73,9 +72,6 @@ data class GenerateTestsModel( val conflictTriggers: ConflictTriggers = ConflictTriggers() - val isMultiPackage: Boolean by lazy { - srcClasses.map { it.packageName }.distinct().size != 1 - } var runGeneratedTestsWithCoverage : Boolean = false var enableSummariesGeneration : Boolean = true diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index 4ad9520208..0587b5076d 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -170,7 +170,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private val testSourceFolderField = TestFolderComboWithBrowseButton(model) - private val codegenLanguages = createComboBox(CodegenLanguage.values()) + private val codegenLanguages = createComboBox(CodegenLanguage.allItems.toTypedArray()) private val testFrameworks = createComboBox(TestFramework.allItems.toTypedArray()) private val mockStrategies = createComboBox(MockStrategyApi.values()) private val staticsMocking = JCheckBox("Mock static methods") @@ -725,6 +725,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> jUnit4LibraryDescriptor(versionInProject) Junit5 -> jUnit5LibraryDescriptor(versionInProject) TestNg -> testNgLibraryDescriptor(versionInProject) + else -> throw IllegalStateException() } selectedTestFramework.isInstalled = true @@ -990,6 +991,8 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> parametrizedTestSources.isEnabled = false Junit5, TestNg -> parametrizedTestSources.isEnabled = true + TestNg -> parametrizedTestSources.isEnabled = true + else -> parametrizedTestSources.isEnabled = false } } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt index 75950f489e..a8c395b385 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/actions/GenerateTestsAction.kt @@ -1,238 +1,15 @@ package org.utbot.intellij.plugin.ui.actions -import com.intellij.openapi.actionSystem.ActionPlaces -import org.utbot.intellij.plugin.generator.UtTestsDialogProcessor -import org.utbot.intellij.plugin.ui.utils.PsiElementHandler import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.actionSystem.UpdateInBackground -import com.intellij.openapi.actionSystem.PlatformDataKeys -import com.intellij.openapi.application.runReadAction -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.module.ModuleUtil -import com.intellij.openapi.project.Project -import com.intellij.openapi.roots.ModuleRootManager -import com.intellij.openapi.roots.ProjectFileIndex -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.psi.* -import com.intellij.psi.util.PsiTreeUtil -import com.intellij.refactoring.util.classMembers.MemberInfo -import org.jetbrains.kotlin.idea.core.getPackage -import org.jetbrains.kotlin.idea.core.util.toPsiDirectory -import org.jetbrains.kotlin.idea.core.util.toPsiFile -import org.utbot.intellij.plugin.util.extractFirstLevelMembers -import org.utbot.intellij.plugin.util.isVisible -import java.util.* -import org.jetbrains.kotlin.j2k.getContainingClass -import org.jetbrains.kotlin.utils.addIfNotNull -import org.utbot.framework.plugin.api.util.LockFile -import org.utbot.intellij.plugin.models.packageName -import org.utbot.intellij.plugin.ui.InvalidClassNotifier -import org.utbot.intellij.plugin.util.isAbstract +import org.utbot.intellij.plugin.language.agnostic.LanguageAssistant -class GenerateTestsAction : AnAction(), UpdateInBackground { +class GenerateTestsAction : AnAction() { override fun actionPerformed(e: AnActionEvent) { - val project = e.project ?: return - - val (srcClasses, focusedMethods, extractMembersFromSrcClasses) = getPsiTargets(e) ?: return - val validatedSrcClasses = validateSrcClasses(srcClasses) ?: return - - UtTestsDialogProcessor.createDialogAndGenerateTests(project, validatedSrcClasses, extractMembersFromSrcClasses, focusedMethods) + LanguageAssistant.get(e)?.actionPerformed(e) } override fun update(e: AnActionEvent) { - if (LockFile.isLocked()) { - e.presentation.isEnabled = false - return - } - if (e.place == ActionPlaces.POPUP) { - e.presentation.text = "Tests with UnitTestBot..." - } - e.presentation.isEnabled = getPsiTargets(e) != null - } - - private fun getPsiTargets(e: AnActionEvent): Triple, Set, Boolean>? { - val project = e.project ?: return null - val editor = e.getData(CommonDataKeys.EDITOR) - if (editor != null) { - //The action is being called from editor - val file = e.getData(CommonDataKeys.PSI_FILE) ?: return null - val element = findPsiElement(file, editor) ?: return null - - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - val srcClass = psiElementHandler.containingClass(element) ?: return null - val srcSourceRoot = srcClass.getSourceRoot() ?: return null - val srcMembers = srcClass.extractFirstLevelMembers(false) - val focusedMethod = focusedMethodOrNull(element, srcMembers, psiElementHandler) - - val module = ModuleUtil.findModuleForFile(srcSourceRoot, project) ?: return null - val matchingRoot = ModuleRootManager.getInstance(module).contentEntries - .flatMap { entry -> entry.sourceFolders.toList() } - .firstOrNull { folder -> folder.file == srcSourceRoot } - if (srcMembers.isEmpty() || matchingRoot == null || matchingRoot.rootType.isForTests) { - return null - } - - return Triple(setOf(srcClass), if (focusedMethod != null) setOf(focusedMethod) else emptySet(), true) - } - } else { - // The action is being called from 'Project' tool window - val srcClasses = mutableSetOf() - val selectedMethods = mutableSetOf() - var extractMembersFromSrcClasses = false - val element = e.getData(CommonDataKeys.PSI_ELEMENT) - if (element is PsiFileSystemItem) { - e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { - srcClasses += getAllClasses(project, it) - } - } else if (element is PsiElement){ - val file = element.containingFile ?: return null - val psiElementHandler = PsiElementHandler.makePsiElementHandler(file) - - if (psiElementHandler.isCreateTestActionAvailable(element)) { - psiElementHandler.containingClass(element)?.let { - srcClasses += setOf(it) - extractMembersFromSrcClasses = true - val memberInfoList = runReadAction> { - it.extractFirstLevelMembers(false) - } - if (memberInfoList.isNullOrEmpty()) - return null - } - - if (element is PsiMethod) { - selectedMethods.add(MemberInfo(element)) - } - } - } else { - val someSelection = e.getData(PlatformDataKeys.SELECTED_ITEMS)?: return null - someSelection.forEach { - when(it) { - is PsiFileSystemItem -> srcClasses += getAllClasses(project, arrayOf(it.virtualFile)) - is PsiClass -> srcClasses.add(it) - is PsiElement -> { - srcClasses.addIfNotNull(it.getContainingClass()) - if (it is PsiMethod) { - selectedMethods.add(MemberInfo(it)) - extractMembersFromSrcClasses = true - } - } - } - } - } - - if (srcClasses.size > 1) { - extractMembersFromSrcClasses = false - } - var commonSourceRoot = null as VirtualFile? - for (srcClass in srcClasses) { - if (commonSourceRoot == null) { - commonSourceRoot = srcClass.getSourceRoot()?: return null - } else if (commonSourceRoot != srcClass.getSourceRoot()) return null - } - if (commonSourceRoot == null) return null - val module = ModuleUtil.findModuleForFile(commonSourceRoot, project)?: return null - - if (!Arrays.stream(ModuleRootManager.getInstance(module).contentEntries) - .flatMap { entry -> Arrays.stream(entry.sourceFolders) } - .filter { folder -> !folder.rootType.isForTests && folder.file == commonSourceRoot} - .findAny().isPresent ) return null - - return Triple(srcClasses.toSet(), selectedMethods.toSet(), extractMembersFromSrcClasses) - } - return null - } - - /** - * Validates that a set of source classes matches some requirements from [isInvalid]. - * If no one of them matches, shows a warning about the first mismatch reason. - */ - private fun validateSrcClasses(srcClasses: Set): Set? { - val filteredClasses = srcClasses - .filterNot { it.isInvalid(withWarnings = false) } - .toSet() - - if (filteredClasses.isEmpty()) { - srcClasses.first().isInvalid(withWarnings = true) - return null - } - - return filteredClasses - } - - private fun PsiClass.isInvalid(withWarnings: Boolean): Boolean { - val isAbstractOrInterface = this.isInterface || this.isAbstract - if (isAbstractOrInterface) { - if (withWarnings) InvalidClassNotifier.notify("abstract class or interface ${this.name}") - return true - } - - val isInvisible = !this.isVisible - if (isInvisible) { - if (withWarnings) InvalidClassNotifier.notify("private or protected class ${this.name}") - return true - } - - val packageIsIncorrect = this.packageName.split(".").firstOrNull() == "java" - if (packageIsIncorrect) { - if (withWarnings) InvalidClassNotifier.notify("class ${this.name} located in java.* package") - return true - } - - return false - } - - private fun PsiElement?.getSourceRoot() : VirtualFile? { - val project = this?.project?: return null - val virtualFile = this.containingFile?.originalFile?.virtualFile?: return null - return ProjectFileIndex.getInstance(project).getSourceRootForFile(virtualFile) - } - - private fun findPsiElement(file: PsiFile, editor: Editor): PsiElement? { - val offset = editor.caretModel.offset - var element = file.findElementAt(offset) - if (element == null && offset == file.textLength) { - element = file.findElementAt(offset - 1) - } - - return element - } - - private fun focusedMethodOrNull(element: PsiElement, methods: List, psiElementHandler: PsiElementHandler): MemberInfo? { - // getParentOfType might return element which does not correspond to the standard Psi hierarchy. - // Thus, make transition to the Psi if it is required. - val currentMethod = PsiTreeUtil.getParentOfType(element, psiElementHandler.methodClass) - ?.let { psiElementHandler.toPsi(it, PsiMethod::class.java) } - - return methods.singleOrNull { it.member == currentMethod } - } - - private fun getAllClasses(directory: PsiDirectory): Set { - val allClasses = directory.files.flatMap { getClassesFromFile(it) }.toMutableSet() - for (subDir in directory.subdirectories) allClasses += getAllClasses(subDir) - return allClasses - } - - private fun getAllClasses(project: Project, virtualFiles: Array): Set { - val psiFiles = virtualFiles.mapNotNull { it.toPsiFile(project) } - val psiDirectories = virtualFiles.mapNotNull { it.toPsiDirectory(project) } - val dirsArePackages = psiDirectories.all { it.getPackage()?.qualifiedName?.isNotEmpty() == true } - - if (!dirsArePackages) { - return emptySet() - } - val allClasses = psiFiles.flatMap { getClassesFromFile(it) }.toMutableSet() - for (psiDir in psiDirectories) allClasses += getAllClasses(psiDir) - - return allClasses - } - - private fun getClassesFromFile(psiFile: PsiFile): List { - val psiElementHandler = PsiElementHandler.makePsiElementHandler(psiFile) - return PsiTreeUtil.getChildrenOfTypeAsList(psiFile, psiElementHandler.classClass) - .map { psiElementHandler.toPsi(it, PsiClass::class.java) } + LanguageAssistant.get(e)?.update(e) } } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/AndroidUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/AndroidUtils.kt new file mode 100644 index 0000000000..7d625e499c --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/AndroidUtils.kt @@ -0,0 +1,41 @@ +package org.utbot.intellij.plugin.ui.utils + +import com.android.tools.idea.gradle.project.GradleProjectInfo +import com.intellij.openapi.project.Project +import com.intellij.openapi.projectRoots.JavaSdk +import com.intellij.openapi.projectRoots.JavaSdkVersion +import com.intellij.openapi.projectRoots.Sdk +import org.jetbrains.android.sdk.AndroidSdkType +import org.utbot.intellij.plugin.ui.CommonErrorNotifier +import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier + +val Project.isBuildWithGradle + get() = GradleProjectInfo.getInstance(this).isBuildWithGradle + +/** + * Obtain JDK version and make sure that it is JDK8 or JDK11 + */ +private fun jdkVersionBy(sdk: Sdk?): JavaSdkVersion { + if (sdk == null) { + CommonErrorNotifier.notify("Failed to obtain JDK version of the project") + } + requireNotNull(sdk) + + val jdkVersion = when (sdk.sdkType) { + is JavaSdk -> { + (sdk.sdkType as JavaSdk).getVersion(sdk) + } + is AndroidSdkType -> { + ((sdk.sdkType as AndroidSdkType).dependencyType as JavaSdk).getVersion(sdk) + } + else -> null + } + if (jdkVersion == null) { + CommonErrorNotifier.notify("Failed to obtain JDK version of the project") + } + requireNotNull(jdkVersion) + if (!jdkVersion.isAtLeast(JavaSdkVersion.JDK_1_8)) { + UnsupportedJdkNotifier.notify(jdkVersion.description) + } + return jdkVersion +} \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/plugin.xml b/utbot-intellij/src/main/resources/META-INF/plugin.xml index 81f3409165..aefc70c92f 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -5,11 +5,12 @@ UnitTestBot utbot.org com.intellij.modules.platform - com.intellij.modules.java - org.jetbrains.kotlin - - org.jetbrains.android + com.intellij.modules.java + com.intellij.modules.lang + org.jetbrains.kotlin + com.intellij.modules.python + org.jetbrains.android + + diff --git a/utbot-intellij/src/main/resources/META-INF/withJS.xml b/utbot-intellij/src/main/resources/META-INF/withJS.xml new file mode 100644 index 0000000000..d04570b311 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withJS.xml @@ -0,0 +1,4 @@ + + + JavaScript + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withJava.xml b/utbot-intellij/src/main/resources/META-INF/withJava.xml new file mode 100644 index 0000000000..2ce2e82cc9 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withJava.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withKotlin.xml b/utbot-intellij/src/main/resources/META-INF/withKotlin.xml new file mode 100644 index 0000000000..07e0e420c3 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withKotlin.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withLang.xml b/utbot-intellij/src/main/resources/META-INF/withLang.xml new file mode 100644 index 0000000000..ed33e791e3 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withLang.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/utbot-intellij/src/main/resources/META-INF/withPython.xml b/utbot-intellij/src/main/resources/META-INF/withPython.xml new file mode 100644 index 0000000000..f272fd7601 --- /dev/null +++ b/utbot-intellij/src/main/resources/META-INF/withPython.xml @@ -0,0 +1,4 @@ + + + com.intellij.modules.python + \ No newline at end of file diff --git a/utbot-js/build.gradle.kts b/utbot-js/build.gradle.kts new file mode 100644 index 0000000000..f868206db9 --- /dev/null +++ b/utbot-js/build.gradle.kts @@ -0,0 +1,55 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + api(project(":utbot-framework")) + implementation(project(":utbot-fuzzers")) + // https://mvnrepository.com/artifact/org.graalvm.js/js + implementation(group = "org.graalvm.js", name = "js", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.js/js-scriptengine + implementation(group = "org.graalvm.js", name = "js-scriptengine", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.truffle/truffle-api + implementation(group = "org.graalvm.truffle", name = "truffle-api", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.graalvm.sdk/graal-sdk + implementation(group = "org.graalvm.sdk", name = "graal-sdk", version = "22.1.0.1") + + // https://mvnrepository.com/artifact/org.json/json + implementation(group = "org.json", name = "json", version = "20220320") + + // https://mvnrepository.com/artifact/commons-io/commons-io + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) +} \ No newline at end of file diff --git a/utbot-js/docs/CLI.md b/utbot-js/docs/CLI.md new file mode 100644 index 0000000000..ee2c5ff0ae --- /dev/null +++ b/utbot-js/docs/CLI.md @@ -0,0 +1,70 @@ +## Build + +.jar file can be built in GitHub Actions with script publish-plugin-and-cli-from-branch. + +## Requirements + +* NodeJs 10.0.0 or higher (available to download https://nodejs.org/en/download/) +* Java 11 or higher (available to download https://www.oracle.com/java/technologies/downloads/) + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_js --source="dir/file_with_sources.js" --output="dir/generated_tests.js" + +This will generate tests for top-level functions from `file_with_sources.js`. + +Run generated tests: + + java -jar utbot-cli.jar run_js --fileOrDir="generated_tests.js" + +This will run generated tests from file or directory. + +Generate coverage report: + + java -jar utbot-cli.jar coverage_js --source=dir/generated_tests.js + +This will generate coverage report from generated tests and print in `StdOut` + +## `generate_js` options + +- `-s, --source ` + + (required) Source code file for a test generation. +- `-c, --class ` + + If not specified tests for top-level functions are generated, otherwise for the specified class. + +- `-o, --output ` + + File for generated tests. +- `-p, --print-test` + + Specifies whether test should be printed out to `StdOut` (default = false) +- `-t, --timeout ` + + Timeout for Node.js to run scripts in seconds (default = 5) + +## `run_js` options + +- `-f, --fileOrDir` + + (required) File or directory with tests. +- `-o, --output` + + Specifies output of .txt file for test framework result (If empty prints to `StdOut`) + +- `-t, --test-framework [mocha]` + + Test framework of tests to run. + +## `coverage_js` options + +- `-s, --source ` + + (required) File with tests to generate a report. + +- `-o, --output` + + Specifies output .json file for generated tests (If empty prints .json to `StdOut`) diff --git a/utbot-js/samples/bitOperators.js b/utbot-js/samples/bitOperators.js new file mode 100644 index 0000000000..3f69723358 --- /dev/null +++ b/utbot-js/samples/bitOperators.js @@ -0,0 +1,31 @@ +class BitOperators { + + complement(x) { + return (~x) === 1 + } + + xor(x, y) { + return (x ^ y) === 0 + } + + and(x) { + return (x & (x - 1)) === 0 + } + + Not(a, b) { + let d = a && b + let e = !a || b + return d && e ? 100 : 200 + } + + shl(x) { + return (x << 1) === 2 + } + + shlWithBigLongShift(shift) { + if (shift < 40) { + return 1 + } + return (0x77777777 << shift) === 0x77777770 ? 2 : 3 + } +} diff --git a/utbot-js/samples/commonIfStatement.js b/utbot-js/samples/commonIfStatement.js new file mode 100644 index 0000000000..addde25d70 --- /dev/null +++ b/utbot-js/samples/commonIfStatement.js @@ -0,0 +1,7 @@ +function foo(a, b) { + if (a > 10) { + return a * b + } else { + return -1 + } +} diff --git a/utbot-js/samples/commonLoops.js b/utbot-js/samples/commonLoops.js new file mode 100644 index 0000000000..cd8d0fd9a2 --- /dev/null +++ b/utbot-js/samples/commonLoops.js @@ -0,0 +1,27 @@ +class Loops { + + whileLoop(value) { + let i = 0 + let sum = 0 + while (i < value) { + sum += i + i += 1 + } + return sum + } + + loopInsideLoop(x) { + for (let i = x - 5; i < x; i++) { + if (i < 0) { + return 2 + } else { + for (let j = i; j < x + i; j++) { + if (j === 7) { + return 1 + } + } + } + } + return -1 + } +} \ No newline at end of file diff --git a/utbot-js/samples/commonRecursion.js b/utbot-js/samples/commonRecursion.js new file mode 100644 index 0000000000..4e7cf3c529 --- /dev/null +++ b/utbot-js/samples/commonRecursion.js @@ -0,0 +1,19 @@ +class Recursion { + factorial(n) { + if (n < 0) + return -1 + if (n === 0) + return 1 + return n * this.factorial(n - 1) + } + + fib(n) { + if (n < 0 || n > 25) + return -1 + if (n === 0) + return 0 + if (n === 1) + return 1 + return this.fib(n - 1) + this.fib(n - 2) + } +} \ No newline at end of file diff --git a/utbot-js/samples/commonString.js b/utbot-js/samples/commonString.js new file mode 100644 index 0000000000..ce9a82a7a6 --- /dev/null +++ b/utbot-js/samples/commonString.js @@ -0,0 +1,19 @@ +class StringExamples { + + isNotBlank(cs) { + return cs.length !== 0 + } + + nullableStringBuffer(buffer, i) { + if (i >= 0) { + buffer += "Positive" + } else { + buffer += "Negative" + } + return buffer.toString() + } + + length(cs) { + return cs == null ? 0 : cs.length + } +} \ No newline at end of file diff --git a/utbot-js/samples/functionsThrowExceptionsInRow.js b/utbot-js/samples/functionsThrowExceptionsInRow.js new file mode 100644 index 0000000000..774d15fd2b --- /dev/null +++ b/utbot-js/samples/functionsThrowExceptionsInRow.js @@ -0,0 +1,24 @@ +// TODO Minor: Support this case, when it comes before normal functions (w/o exception) +function timeoutEx(a) { + while (true) { + } +} + +function customError(a) { + if (a > 5) { + throw Error("MyCustomError") + } else { + return 10 + } +} + +function goodBoy(a) { + switch (a) { + case 5: + return 5 + case 10: + return 10 + default: + return 0 + } +} diff --git a/utbot-js/samples/scenarioMultyClassNoTopLevel.js b/utbot-js/samples/scenarioMultyClassNoTopLevel.js new file mode 100644 index 0000000000..c37ce365e3 --- /dev/null +++ b/utbot-js/samples/scenarioMultyClassNoTopLevel.js @@ -0,0 +1,39 @@ +class Na { + constructor(num) { + this.num = num + } + + double() { + return this.num * 2 + } + + static test(a, b) { + return a + 2 * b + } +} + +class Kek { + foo(a, b) { + return a + b + } + + fString(a, b) { + return a + b + } + + fDel(a, b) { + return a / b + } + + fObj(a, b) { + return a.num + b.num + } + + getDone(a) { + a.done() + } + + done() { + return this.toString() + } +} \ No newline at end of file diff --git a/utbot-js/samples/scenarioObjectParameter.js b/utbot-js/samples/scenarioObjectParameter.js new file mode 100644 index 0000000000..c579422ccb --- /dev/null +++ b/utbot-js/samples/scenarioObjectParameter.js @@ -0,0 +1,14 @@ +class ObjectParameter { + + constructor(a) { + this.first = a + } + + performAction(value) { + return 2 * value + } +} + +function functionToTest(obj, v) { + return obj.performAction(v) +} \ No newline at end of file diff --git a/utbot-js/samples/scenarioStaticMethod.js b/utbot-js/samples/scenarioStaticMethod.js new file mode 100644 index 0000000000..783937e859 --- /dev/null +++ b/utbot-js/samples/scenarioStaticMethod.js @@ -0,0 +1,13 @@ +class Object { + + constructor(a) { + this.first = a + } + + static functionToTest(value) { + if (value > 1024 && value < 1026) { + return 2 * value + } + return value + } +} diff --git a/utbot-js/samples/scenarioThrowError.js b/utbot-js/samples/scenarioThrowError.js new file mode 100644 index 0000000000..416c43d1bb --- /dev/null +++ b/utbot-js/samples/scenarioThrowError.js @@ -0,0 +1,11 @@ +function functionToTest(a) { + if (a === true) { + throw Error("err") + } else if (a === 1) { + while (true) { + } + } else { + return -1 + } + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt new file mode 100644 index 0000000000..3cbf51fd66 --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -0,0 +1,487 @@ +package api + +import codegen.JsCodeGenerator +import com.oracle.js.parser.ErrorManager +import com.oracle.js.parser.Parser +import com.oracle.js.parser.ScriptEnvironment +import com.oracle.js.parser.Source +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import com.oracle.truffle.api.strings.TruffleString +import fuzzer.JsFuzzer +import fuzzer.providers.JsObjectModelProvider +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.plugin.api.EnvironmentModels +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtExplicitlyThrownException +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsMethodId +import org.utbot.framework.plugin.api.js.JsMultipleClassId +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.isJsBasic +import org.utbot.framework.plugin.api.js.util.jsErrorClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import parser.JsClassAstVisitor +import parser.JsFunctionAstVisitor +import parser.JsFuzzerAstVisitor +import parser.JsParserUtils +import parser.JsToplevelFunctionAstVisitor +import service.CoverageService +import service.ServiceContext +import service.TernService +import settings.JsTestGenerationSettings.dummyClassName +import settings.JsTestGenerationSettings.fileUnderTestAliases +import settings.JsTestGenerationSettings.functionCallResultAnchor +import settings.JsTestGenerationSettings.tempFileName +import utils.JsCmdExec +import utils.PathResolver +import utils.constructClass +import utils.toJsAny +import java.io.File +import java.util.Collections + + +class JsTestGenerator( + private val fileText: String, + private var sourceFilePath: String, + private var projectPath: String = sourceFilePath.replaceAfterLast(File.separator, ""), + private val selectedMethods: List? = null, + private var parentClassName: String? = null, + private var outputFilePath: String?, + private val exportsManager: (List) -> Unit, + private val timeout: Long, +) { + + private val exports = mutableSetOf() + + private lateinit var parsedFile: FunctionNode + + private val utbotDir = "utbotJs" + + init { + fixPathDelims() + } + + private fun fixPathDelims() { + projectPath = projectPath.replace("\\", "/") + outputFilePath = outputFilePath?.replace("\\", "/") + sourceFilePath = sourceFilePath.replace("\\", "/") + } + + /** + * Returns String representation of generated tests. + */ + fun run(): String { + parsedFile = runParser(fileText) + val context = ServiceContext( + utbotDir = utbotDir, + projectPath = projectPath, + filePathToInference = sourceFilePath, + trimmedFileText = fileText, + fileText = fileText, + nodeTimeout = timeout, + ) + val ternService = TernService(context) + val paramNames = mutableMapOf>() + val testSets = mutableListOf() + val classNode = + JsParserUtils.searchForClassDecl( + className = parentClassName, + fileText = fileText, + strict = selectedMethods?.isNotEmpty() ?: false + ) + parentClassName = classNode?.ident?.name?.toString() + val classId = makeJsClassId(classNode, ternService) + val methods = makeMethodsToTest() + if (methods.isEmpty()) throw IllegalArgumentException("No methods to test were found!") + methods.forEach { funcNode -> + try { + makeTestsForMethod(classId, funcNode, classNode, context, testSets, paramNames) + } catch (_: Exception) { + + } + } + val importPrefix = makeImportPrefix() + val codeGen = JsCodeGenerator( + classUnderTest = classId, + paramNames = paramNames, + importPrefix = importPrefix + ) + return codeGen.generateAsStringWithTestReport(testSets).generatedCode + } + + private fun makeTestsForMethod( + classId: JsClassId, + funcNode: FunctionNode, + classNode: ClassNode?, + context: ServiceContext, + testSets: MutableList, + paramNames: MutableMap> + ) { + val execId = classId.allMethods.find { + it.name == funcNode.name.toString() + } ?: throw IllegalStateException() + manageExports(classNode, funcNode, execId) + val (concreteValues, fuzzedValues) = runFuzzer(funcNode, execId) + val coveredBranchesArray = Array>(fuzzedValues.size) { emptySet() } + val timeoutErrors = + runCoverageAnalysis(context, fuzzedValues, execId, classNode, coveredBranchesArray) + val testsForGenerator = mutableListOf() + val resultRegex = Regex("$functionCallResultAnchor (.*)") + val errorResultRegex = Regex(".*(Error: .*)") + val illegalStateExceptionError = if (timeoutErrors.size == fuzzedValues.size) + mapOf("No successful tests were generated! Please check the function under test." to 1) + else + emptyMap() + val errorsForGenerator = timeoutErrors.associate { + "Timeout in generating test for ${fuzzedValues[it].joinToString { f -> f.model.toString() }} parameters" to 1 + } + illegalStateExceptionError + analyzeCoverage(coveredBranchesArray.toList()).forEach { paramIndex -> + val param = fuzzedValues[paramIndex] + val result = + getUtModelResult(param, execId, classNode, File(sourceFilePath).name, resultRegex, errorResultRegex) + val thisInstance = makeThisInstance(execId, classId, concreteValues) + val initEnv = EnvironmentModels(thisInstance, param.map { it.model }, mapOf()) + testsForGenerator.add( + UtExecution( + stateBefore = initEnv, + stateAfter = initEnv, + result = result, + ) + ) + } + val testSet = CgMethodTestSet( + execId, + testsForGenerator, + errorsForGenerator + ) + testSets += testSet + paramNames[execId] = funcNode.parameters.map { it.name.toString() } + } + + private fun makeImportPrefix(): String { + val importPrefix = outputFilePath?.let { + PathResolver.getRelativePath( + File(it).parent, + File(sourceFilePath).parent, + ) + } ?: "" + return importPrefix + } + + private fun makeThisInstance( + execId: JsMethodId, + classId: JsClassId, + concreteValues: Set + ): UtModel? { + val thisInstance = when { + execId.isStatic -> null + classId.allConstructors.first().parameters.isEmpty() -> { + val id = JsObjectModelProvider.idGenerator.asInt + val constructor = classId.allConstructors.first() + val instantiationChain = mutableListOf() + UtAssembleModel( + id = id, + classId = constructor.classId, + modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationChain = instantiationChain + ).apply { + instantiationChain += UtExecutableCallModel( + instance = null, + executable = constructor, + params = emptyList(), + returnValue = this + ) + } + } + + else -> { + JsObjectModelProvider.generate( + FuzzedMethodDescription( + name = "thisInstance", + returnType = voidClassId, + parameters = listOf(classId), + concreteValues = concreteValues + ) + ).take(10).toList() + .shuffled().map { it.value.model }.first() + } + } + return thisInstance + } + + private fun getUtModelResult( + param: List, + execId: JsMethodId, + classNode: ClassNode?, + importText: String, + resultRegex: Regex, + errorResultRegex: Regex + ): UtExecutionResult { + val utConstructor = JsUtModelConstructor() + val scriptText = + makeStringForRunJs(param, execId, classNode?.ident?.name, importText) + val returnText = runJs( + scriptText, + File(sourceFilePath).parent, + ) + val unparsedValueSeq = resultRegex.findAll(returnText) + val errorSeq = errorResultRegex.findAll(returnText) + val unparsedValue = if (unparsedValueSeq.any()) { + unparsedValueSeq.last().groups[1]?.value ?: throw IllegalStateException() + } else { + errorSeq.last().groups[1]?.value ?: throw IllegalStateException() + } + val (returnValue, valueClassId) = unparsedValue.toJsAny(execId.returnType) + val result = utConstructor.construct(returnValue, valueClassId) + val utExecResult = when (result.classId) { + jsErrorClassId -> UtExplicitlyThrownException(Throwable(returnValue.toString()), false) + else -> UtExecutionSuccess(result) + } + return utExecResult + } + + private fun runCoverageAnalysis( + context: ServiceContext, + fuzzedValues: List>, + execId: JsMethodId, + classNode: ClassNode?, + coveredBranchesArray: Array> + ): MutableList { + val timeoutErrors = Collections.synchronizedList(mutableListOf()) + val basicCoverageService = CoverageService( + context = context, + scriptText = context.trimmedFileText, + id = 1024, + originalFileName = File(sourceFilePath).name, + newFileName = File(sourceFilePath).name, + errors = mutableListOf() + ) + val basicCoverage = basicCoverageService.getCoveredLines() + basicCoverageService.removeTempFiles() + fuzzedValues.indices.toList().parallelStream().forEach { + val scriptText = + makeStringForRunJs( + fuzzedValue = fuzzedValues[it], + method = execId, + containingClass = classNode?.ident?.name, + importText = File(context.filePathToInference).name, + ) + val coverageService = CoverageService( + context = context, + scriptText = scriptText, + id = it, + originalFileName = File(sourceFilePath).name, + newFileName = tempFileName, + basicCoverage = basicCoverage, + errors = timeoutErrors, + ) + coveredBranchesArray[it] = coverageService.getCoveredLines().toSet() + coverageService.removeTempFiles() + } + return timeoutErrors + } + + private fun runFuzzer( + funcNode: FunctionNode, + execId: JsMethodId + ): Pair, List>> { + val fuzzerVisitor = JsFuzzerAstVisitor() + funcNode.body.accept(fuzzerVisitor) + val methodUnderTestDescription = + FuzzedMethodDescription(execId, fuzzerVisitor.fuzzedConcreteValues).apply { + compilableName = funcNode.name.toString() + val names = funcNode.parameters.map { it.name.toString() } + parameterNameMap = { index -> names.getOrNull(index) } + } + val fuzzedValues = + JsFuzzer.jsFuzzing(methodUnderTestDescription = methodUnderTestDescription).toList() + .shuffled() + .take(1_000) + return Pair(fuzzerVisitor.fuzzedConcreteValues.toSet(), fuzzedValues) + } + + private fun manageExports( + classNode: ClassNode?, + funcNode: FunctionNode, + execId: JsMethodId + ) { + val obligatoryExport = (classNode?.ident?.name ?: funcNode.ident.name).toString() + val collectedExports = collectExports(execId) + exports += (collectedExports + obligatoryExport) + exportsManager(exports.toList()) + } + + private fun makeMethodsToTest(): List { + val methods = selectedMethods?.map { + getFunctionNode( + focusedMethodName = it, + parentClassName = parentClassName, + fileText = fileText + ) + } ?: getMethodsToTest() + return methods + } + + private fun makeJsClassId( + classNode: ClassNode?, + ternService: TernService + ): JsClassId { + val classId = classNode?.let { + JsClassId(parentClassName!!).constructClass(ternService, classNode) + } ?: JsClassId("undefined").constructClass( + ternService = ternService, + functions = extractToplevelFunctions() + ) + return classId + } + + private fun runParser(fileText: String): FunctionNode { + val parser = Parser( + ScriptEnvironment.builder().build(), + Source.sourceFor("jsFile", fileText), + ErrorManager.ThrowErrorManager() + ) + return parser.parse() + } + + private fun extractToplevelFunctions(): List { + val visitor = JsToplevelFunctionAstVisitor() + parsedFile.body.accept(visitor) + return visitor.extractedMethods + } + + private fun collectExports(methodId: JsMethodId): List { + val res = mutableListOf() + methodId.parameters.forEach { + if (!(it.isJsBasic || it is JsMultipleClassId)) { + res += it.name + } + } + if (!(methodId.returnType.isJsBasic || methodId.returnType is JsMultipleClassId)) res += methodId.returnType.name + return res + } + + private fun runJs(scriptText: String, workDir: String): String { + val (reader, errorReader) = JsCmdExec.runCommand( + cmd = "node -e \"$scriptText\"", + dir = workDir, + shouldWait = true, + timeout = timeout + ) + return errorReader.readText().ifEmpty { reader.readText() } + } + + private fun makeStringForRunJs( + fuzzedValue: List, + method: JsMethodId, + containingClass: TruffleString?, + importText: String, + ): String { + val callString = makeCallFunctionString(fuzzedValue, method, containingClass) + val prefix = functionCallResultAnchor + val temp = "console.log(`$prefix \\\"\${res}\\\"`)" + return "const $fileUnderTestAliases = require(\\\"./$importText\\\"); " + + "let prefix = \\\"$prefix\\\"; " + + "let res = $callString; " + + "if (typeof res == \\\"string\\\") {$temp} else console.log(prefix, res)" + } + + private fun makeCallFunctionString( + fuzzedValue: List, + method: JsMethodId, + containingClass: TruffleString? + ): String { + val initClass = containingClass?.let { + if (!method.isStatic) { + "new $fileUnderTestAliases.${it}()." + } else "$fileUnderTestAliases.$it." + } ?: "$fileUnderTestAliases." + var callString = "$initClass${method.name}" + callString += fuzzedValue.joinToString( + prefix = "(", + postfix = ")", + ) { value -> value.model.toCallString() } + return callString + } + + private fun Any.quoteWrapIfNecessary(): String = + when (this) { + is String -> "\"$this\"" + else -> "$this" + } + + private fun UtAssembleModel.toParamString(): String = + with(this) { + val callConstructorString = "new $fileUnderTestAliases.${classId.name}" + val paramsString = (instantiationChain.first() as UtExecutableCallModel).params.joinToString( + prefix = "(", + postfix = ")", + ) { + (it as JsPrimitiveModel).value.quoteWrapIfNecessary() + } + return callConstructorString + paramsString + } + + private fun UtModel.toCallString(): String = + when (this) { + is UtAssembleModel -> this.toParamString() + else -> { + (this as JsPrimitiveModel).value.quoteWrapIfNecessary() + } + } + + + private fun analyzeCoverage(coverageList: List>): List { + val allCoveredBranches = mutableSetOf() + val resultList = mutableListOf() + coverageList.forEachIndexed { index, it -> + if (!allCoveredBranches.containsAll(it)) { + resultList += index + allCoveredBranches.addAll(it) + } + } + return resultList + } + + private fun getFunctionNode(focusedMethodName: String, parentClassName: String?, fileText: String): FunctionNode { + val parser = Parser( + ScriptEnvironment.builder().build(), + Source.sourceFor("jsFile", fileText), + ErrorManager.ThrowErrorManager() + ) + val fileNode = parser.parse() + val visitor = JsFunctionAstVisitor( + focusedMethodName, + if (parentClassName != dummyClassName) parentClassName else null + ) + fileNode.accept(visitor) + return visitor.targetFunctionNode + } + + private fun getMethodsToTest() = + parentClassName?.let { + getClassMethods(it) + } ?: extractToplevelFunctions().ifEmpty { + getClassMethods("") + } + + private fun getClassMethods(className: String): List { + val visitor = JsClassAstVisitor(className) + parsedFile.body.accept(visitor) + val classNode = JsParserUtils.searchForClassDecl(className, fileText) + return classNode?.classElements?.filter { + it.value is FunctionNode + }?.map { it.value as FunctionNode } ?: throw IllegalStateException("Can't extract methods of class $className") + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt new file mode 100644 index 0000000000..8f0100f4f7 --- /dev/null +++ b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt @@ -0,0 +1,66 @@ +package api + +import fuzzer.providers.JsObjectModelProvider +import org.utbot.framework.concrete.UtModelConstructorInterface +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsEmptyClassId +import org.utbot.framework.plugin.api.js.JsNullModel +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.JsUndefinedModel +import org.utbot.framework.plugin.api.js.util.jsErrorClassId +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId + +class JsUtModelConstructor : UtModelConstructorInterface { + + // TODO SEVERE: This is a very dirty prototype version. Expand! + @Suppress("NAME_SHADOWING") + override fun construct(value: Any?, classId: ClassId): UtModel { + val classId = classId as JsClassId + when (classId) { + jsUndefinedClassId -> return JsUndefinedModel(classId) + jsErrorClassId -> return UtModel(jsErrorClassId) + } + return when (value) { + null -> JsNullModel(classId) + is Byte, + is Short, + is Char, + is Int, + is Long, + is Float, + is Double, + is String, + is Boolean -> JsPrimitiveModel(value) + + is Map<*, *> -> { + constructObject(classId, value) + } + + else -> JsUndefinedModel(classId) + } + } + + @Suppress("UNCHECKED_CAST") + private fun constructObject(classId: JsClassId, value: Any?): UtModel { + val constructor = classId.allConstructors.first() + val values = (value as Map).values.map { + construct(it, JsEmptyClassId()) + } + val id = JsObjectModelProvider.idGenerator.asInt + val instantiationChain = mutableListOf() + return UtAssembleModel( + id = id, + classId = constructor.classId, + modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationChain = instantiationChain, + modificationsChain = mutableListOf() + ).apply { + instantiationChain += UtExecutableCallModel(null, constructor, values, this) + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt new file mode 100644 index 0000000000..1750f283d6 --- /dev/null +++ b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt @@ -0,0 +1,86 @@ +package codegen + +import framework.codegen.JsCodeLanguage +import framework.codegen.Mocha +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.HangingTestsTimeout +import org.utbot.framework.codegen.ParametrizedTestSource +import org.utbot.framework.codegen.RegularImport +import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour +import org.utbot.framework.codegen.StaticsMocking +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.model.TestsCodeWithTestReport +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.tree.CgTestClassFile +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.js.JsClassId +import settings.JsTestGenerationSettings.fileUnderTestAliases + +class JsCodeGenerator( + private val classUnderTest: JsClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = Mocha, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName, + importPrefix: String, +) { + private var context: CgContext = CgContext( + classUnderTest = classUnderTest, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = MockFramework.MOCKITO, + codegenLanguage = CodegenLanguage.JS, + codeGenLanguage = JsCodeLanguage, + parametrizedTestSource = ParametrizedTestSource.defaultItem, + staticsMocking = StaticsMocking.defaultItem, + forceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking = true, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName, + collectedImports = mutableSetOf( + RegularImport("assert", "assert"), + RegularImport( + fileUnderTestAliases, + "./$importPrefix/${classUnderTest.filePath.substringAfterLast("/")}" + ) + ) + ) + + fun generateAsStringWithTestReport( + cgTestSets: List, + testClassCustomName: String? = null, + ): TestsCodeWithTestReport = withCustomContext(testClassCustomName) { + val testClassModel = TestClassModel(classUnderTest, cgTestSets) + val testClassFile = CgTestClassConstructor(context).construct(testClassModel) + TestsCodeWithTestReport(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + } + + private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { + val prevContext = context + return try { + context = prevContext.copy( + shouldOptimizeImports = true, + testClassCustomName = testClassCustomName + ) + block() + } finally { + context = prevContext + } + } + + private fun renderClassFile(file: CgTestClassFile): String { + val renderer = CgAbstractRenderer.makeRenderer(context) + file.accept(renderer) + return renderer.toString() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt new file mode 100644 index 0000000000..359b66bc1a --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt @@ -0,0 +1,60 @@ +package framework.codegen + +import framework.codegen.model.constructor.tree.JsCgCallableAccessManager +import framework.codegen.model.constructor.tree.JsCgMethodConstructor +import framework.codegen.model.constructor.tree.JsCgStatementConstructor +import framework.codegen.model.constructor.tree.JsCgVariableConstructor +import framework.codegen.model.constructor.tree.MochaManager +import framework.codegen.model.constructor.visitor.CgJsRenderer +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodeGenLanguage +import org.utbot.framework.plugin.api.utils.testClassNameGenerator + +object JsCodeLanguage : CodeGenLanguage() { + override val outerMostTestClassContent: TestClassContext = TestClassContext() + override val displayName: String = "JavaScript" + + override val extension: String + get() = ".js" + + override val languageKeywords: Set = setOf( + "abstract", "arguments", "await", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", + "debugger", "default", "delete", "do", "double", "else", "enum", "eval", "export", "extends", "false", "final", + "finally", "float", "for", "function", "goto", "if", "implements", "import", "in", "instanceof", "int", "interface", + "let", "long", "native", "new", "null", "package", "private", "protected", "public", "return", "short", "static", + "super", "switch", "synchronized", "this", "throw", "throws", "transient", "true", "try", "typeof", "var", "void", + "volatile", "while", "with", "yield" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) + } + + override fun cgRenderer(context: CgContext, printer: CgPrinter): CgAbstractRenderer = CgJsRenderer(context, printer) + + override fun getCallableAccessManagerBy(context: CgContext) = JsCgCallableAccessManager(context) + + override fun getMethodConstructorBy(context: CgContext) = JsCgMethodConstructor(context) + + override fun getStatementConstructorBy(context: CgContext) = JsCgStatementConstructor(context) + + override fun getVariableConstructorBy(context: CgContext) = JsCgVariableConstructor(context) + + override val testFrameworks = listOf(Mocha) + + override fun managerByFramework(context: CgContext): TestFrameworkManager = when (context.testFramework) { + is Mocha -> MochaManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Mocha +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt new file mode 100644 index 0000000000..17ea549ec9 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt @@ -0,0 +1,64 @@ +package framework.codegen + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.util.jsErrorClassId +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId + + +object Mocha : TestFramework("Mocha") { + override val mainPackage = "" + override val assertionsClass = jsUndefinedClassId + override val arraysAssertionsClass = jsUndefinedClassId + override val testAnnotation = "" + override val testAnnotationFqn = "" + + override val parameterizedTestAnnotation = "Parameterized tests are not supported for Mocha" + override val parameterizedTestAnnotationFqn = "Parameterized tests are not supported for Mocha" + override val methodSourceAnnotation = "Parameterized tests are not supported for Mocha" + override val methodSourceAnnotationFqn = "Parameterized tests are not supported for Mocha" + + //TODO MINOR: think + override val nestedClassesShouldBeStatic: Boolean + get() = false + override val argListClassId: ClassId + get() = jsUndefinedClassId + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val testAnnotationId = BuiltinClassId( + name = "Mocha", + canonicalName = "Mocha", + simpleName = "Test" + ) + + override val parameterizedTestAnnotationId = jsUndefinedClassId + override val methodSourceAnnotationId = jsUndefinedClassId +} + +internal val jsAssertEquals by lazy { + BuiltinMethodId( + JsClassId("assert.deepEqual"), "assert.deepEqual", jsUndefinedClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId + ) + ) +} + +internal val jsAssertThrows by lazy { + BuiltinMethodId( + JsClassId("assert.throws"), "assert.throws", jsErrorClassId, listOf( + jsUndefinedClassId, jsUndefinedClassId, jsUndefinedClassId + ) + ) +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt new file mode 100644 index 0000000000..cb5a005354 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt @@ -0,0 +1,50 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgIncompleteMethodCall +import org.utbot.framework.codegen.model.tree.CgConstructorCall +import org.utbot.framework.codegen.model.tree.CgExecutableCall +import org.utbot.framework.codegen.model.tree.CgExpression +import org.utbot.framework.codegen.model.tree.CgMethodCall +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.MethodId + +class JsCgCallableAccessManager(context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override operator fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override operator fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, null) + + override operator fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = args.resolve() + val methodCall = CgMethodCall(caller, method, resolvedArgs) + newMethodCall(method) + return methodCall + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importedClasses += constructorId.classId + } + + private fun newMethodCall(methodId: MethodId) { + if (methodId.classId.name == "undefined") { + importedStaticMethods += methodId + return + } + importedClasses += methodId.classId + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt new file mode 100644 index 0000000000..3aac5ee0a9 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgMethodConstructor.kt @@ -0,0 +1,133 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor +import org.utbot.framework.codegen.model.tree.CgTestMethod +import org.utbot.framework.codegen.model.tree.CgTestMethodType +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.plugin.api.ConcreteExecutionFailureException +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.onFailure +import org.utbot.framework.plugin.api.onSuccess +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.util.isUnit +import java.security.AccessControlException + +class JsCgMethodConstructor(ctx: CgContext) : CgMethodConstructor(ctx) { + + override fun assertEquality(expected: CgValue, actual: CgVariable) { + testFrameworkManager.assertEquals(expected, actual) + } + + override fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) + execution.displayName = execution.displayName?.let { "${executableId.name}: $it" } + testMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + val mainBody = { + substituteStaticFields(statics) + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[executableId]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } + recordActualResult() + generateResultAssertions() + generateFieldStateAssertions() + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + override fun generateResultAssertions() { + emptyLineIfNeeded() + val currentExecution = currentExecution!! + val method = currentExecutable as MethodId + // build assertions + currentExecution.result + .onSuccess { result -> + methodType = CgTestMethodType.SUCCESSFUL + if (result.isUnit() || method.returnType == voidClassId) { + +thisInstance[method](*methodArguments.toTypedArray()) + } else { + resultModel = result + val expected = variableConstructor.getOrCreateVariable(result, "expected") + assertEquality(expected, actual) + } + } + .onFailure { exception -> + processExecutionFailure(currentExecution, exception) + } + } + + private fun processExecutionFailure(execution: UtExecution, exception: Throwable) { + val methodInvocationBlock = { + with(currentExecutable) { + when (this) { + is MethodId -> thisInstance[this](*methodArguments.toTypedArray()).intercepted() + is ConstructorId -> this(*methodArguments.toTypedArray()).intercepted() + else -> throw IllegalStateException() + } + } + } + + if (shouldTestPassWithException(execution, exception)) { + testFrameworkManager.expectException(JsClassId(exception.message!!)) { + methodInvocationBlock() + } + methodType = CgTestMethodType.SUCCESSFUL + + return + } + + if (shouldTestPassWithTimeoutException(execution, exception)) { + writeWarningAboutTimeoutExceeding() + testFrameworkManager.expectTimeout(hangingTestsTimeout.timeoutMs) { + methodInvocationBlock() + } + methodType = CgTestMethodType.TIMEOUT + + return + } + + when (exception) { + is ConcreteExecutionFailureException -> { + methodType = CgTestMethodType.CRASH + writeWarningAboutCrash() + } + + is AccessControlException -> { + methodType = CgTestMethodType.CRASH + writeWarningAboutFailureTest(exception) + return + } + + else -> { + methodType = CgTestMethodType.FAILING + writeWarningAboutFailureTest(exception) + } + } + + methodInvocationBlock() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt new file mode 100644 index 0000000000..0847bdef10 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt @@ -0,0 +1,270 @@ +package framework.codegen.model.constructor.tree + +import fj.data.Either +import framework.codegen.model.constructor.util.plus +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType +import org.utbot.framework.codegen.model.tree.CgAnnotation +import org.utbot.framework.codegen.model.tree.CgAnonymousFunction +import org.utbot.framework.codegen.model.tree.CgComment +import org.utbot.framework.codegen.model.tree.CgDeclaration +import org.utbot.framework.codegen.model.tree.CgEmptyLine +import org.utbot.framework.codegen.model.tree.CgExpression +import org.utbot.framework.codegen.model.tree.CgForEachLoopBuilder +import org.utbot.framework.codegen.model.tree.CgForLoopBuilder +import org.utbot.framework.codegen.model.tree.CgIfStatement +import org.utbot.framework.codegen.model.tree.CgInnerBlock +import org.utbot.framework.codegen.model.tree.CgIsInstance +import org.utbot.framework.codegen.model.tree.CgLogicalAnd +import org.utbot.framework.codegen.model.tree.CgLogicalOr +import org.utbot.framework.codegen.model.tree.CgMultilineComment +import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument +import org.utbot.framework.codegen.model.tree.CgParameterDeclaration +import org.utbot.framework.codegen.model.tree.CgReturnStatement +import org.utbot.framework.codegen.model.tree.CgSingleArgAnnotation +import org.utbot.framework.codegen.model.tree.CgSingleLineComment +import org.utbot.framework.codegen.model.tree.CgThrowStatement +import org.utbot.framework.codegen.model.tree.CgTryCatch +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.tree.buildAssignment +import org.utbot.framework.codegen.model.tree.buildDeclaration +import org.utbot.framework.codegen.model.tree.buildDoWhileLoop +import org.utbot.framework.codegen.model.tree.buildForLoop +import org.utbot.framework.codegen.model.tree.buildTryCatch +import org.utbot.framework.codegen.model.tree.buildWhileLoop +import org.utbot.framework.codegen.model.util.buildExceptionHandler +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel + +class JsCgStatementConstructor(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by CgComponents.getCallableAccessManagerBy(context) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName, isMock) + + // TODO SEVERE: here was import section for CgClassId. Implement it +// importIfNeeded(baseType) +// if ((baseType as JsClassId).name != "undefined") { +// importedClasses += baseType +// } + + val declaration = buildDeclaration { + variableType = baseType + variableName = name + initializer = baseExpr + isMutable = isMutableVar + } + + updateVariableScope(declaration.variable, model) + + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) { + throw UnsupportedOperationException("JavaScript does not have forEach loops") + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.decapitalize())) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance { + TODO("Not yet implemented") + } + + // TODO MINOR: check whether js has inner blocks + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun annotation(classId: ClassId, argument: Any?): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve()) + addAnnotation(annotation) + return annotation + } + + override fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation { + val annotation = CgMultipleArgsAnnotation( + classId, + namedArguments.mapTo(mutableListOf()) { (name, value) -> CgNamedAnnotationArgument(name, value) } + ) + addAnnotation(annotation) + return annotation + } + + override fun annotation( + classId: ClassId, + buildArguments: MutableList>.() -> Unit + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList()) + addAnnotation(annotation) + return annotation + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine() + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + updateVariableScope(it) + } + + // TODO SEVERE: think about these 2 functions + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType = + ExpressionWithType(baseType, expression) + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = baseType +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt new file mode 100644 index 0000000000..bb10697b89 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt @@ -0,0 +1,36 @@ +package framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor +import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.tree.CgLiteral +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.util.nullLiteral +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtCompositeModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtReferenceModel +import org.utbot.framework.plugin.api.js.JsPrimitiveModel + +class JsCgVariableConstructor(ctx: CgContext) : CgVariableConstructor(ctx) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(ctx) + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + return if (model is UtReferenceModel) valueByModelId.getOrPut(model.id) { + when (model) { + is UtCompositeModel -> TODO() + is UtAssembleModel -> constructAssemble(model, baseName) + is UtArrayModel -> TODO() + else -> TODO() + } + } else valueByModel.getOrPut(model) { + when (model) { + is JsPrimitiveModel -> CgLiteral(model.classId, model.value) + else -> nullLiteral() + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt new file mode 100644 index 0000000000..7a6f6bad46 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsTestFrameworkManager.kt @@ -0,0 +1,56 @@ +package framework.codegen.model.constructor.tree + +import framework.codegen.Mocha +import framework.codegen.jsAssertEquals +import framework.codegen.jsAssertThrows +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.tree.CgAnnotation +import org.utbot.framework.codegen.model.tree.CgValue +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.plugin.api.ClassId + +internal class MochaManager(context: CgContext) : TestFrameworkManager(context) { + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Mocha) { "According to settings, Mocha.js was expected, but got: $testFramework" } + val lambda = statementConstructor.lambda(exception) { block() } + +assertions[jsAssertThrows](lambda, "Error", exception.name) + } + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) { + TODO("Not yet implemented") + } + + override val dataProviderMethodsHolder: TestClassContext + get() = TODO("Not yet implemented") + override val annotationForNestedClasses: CgAnnotation? + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation? + get() = TODO("Not yet implemented") + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +assertions[jsAssertEquals](expected, actual) + } + + override fun disableTestMethod(reason: String) { + + } + +} diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt new file mode 100644 index 0000000000..4b33d306c2 --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,16 @@ +package framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt new file mode 100644 index 0000000000..b56036556a --- /dev/null +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -0,0 +1,386 @@ +package framework.codegen.model.constructor.visitor + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.codegen.RegularImport +import org.utbot.framework.codegen.StaticImport +import org.utbot.framework.codegen.isLanguageKeyword +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.tree.CgAllocateArray +import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray +import org.utbot.framework.codegen.model.tree.CgAnonymousFunction +import org.utbot.framework.codegen.model.tree.CgArrayAnnotationArgument +import org.utbot.framework.codegen.model.tree.CgArrayElementAccess +import org.utbot.framework.codegen.model.tree.CgArrayInitializer +import org.utbot.framework.codegen.model.tree.CgConstructorCall +import org.utbot.framework.codegen.model.tree.CgDeclaration +import org.utbot.framework.codegen.model.tree.CgEqualTo +import org.utbot.framework.codegen.model.tree.CgErrorTestMethod +import org.utbot.framework.codegen.model.tree.CgErrorWrapper +import org.utbot.framework.codegen.model.tree.CgExecutableCall +import org.utbot.framework.codegen.model.tree.CgExpression +import org.utbot.framework.codegen.model.tree.CgFieldAccess +import org.utbot.framework.codegen.model.tree.CgForLoop +import org.utbot.framework.codegen.model.tree.CgGetJavaClass +import org.utbot.framework.codegen.model.tree.CgGetKotlinClass +import org.utbot.framework.codegen.model.tree.CgGetLength +import org.utbot.framework.codegen.model.tree.CgInnerBlock +import org.utbot.framework.codegen.model.tree.CgLiteral +import org.utbot.framework.codegen.model.tree.CgMethod +import org.utbot.framework.codegen.model.tree.CgMethodCall +import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation +import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument +import org.utbot.framework.codegen.model.tree.CgNotNullAssertion +import org.utbot.framework.codegen.model.tree.CgParameterDeclaration +import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod +import org.utbot.framework.codegen.model.tree.CgSpread +import org.utbot.framework.codegen.model.tree.CgStaticsRegion +import org.utbot.framework.codegen.model.tree.CgSwitchCase +import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel +import org.utbot.framework.codegen.model.tree.CgTestClass +import org.utbot.framework.codegen.model.tree.CgTestClassFile +import org.utbot.framework.codegen.model.tree.CgTestMethod +import org.utbot.framework.codegen.model.tree.CgThrowStatement +import org.utbot.framework.codegen.model.tree.CgTypeCast +import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.util.CgPrinterImpl +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.TypeParameters +import settings.JsTestGenerationSettings.fileUnderTestAliases + +internal class CgJsRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer) { + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "&&" + + override val logicalOr: String + get() = "||" + + override val language: CodegenLanguage = CodegenLanguage.JS + + override val langPackage: String = "js" + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + print("alert(\"${element.message}\")") + } + + override fun visit(element: CgInnerBlock) { + println("{") + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + println("}") + } + + override fun visit(element: CgParameterDeclaration) { + if (element.isVararg) { + print("...") + } + print(element.name.escapeNamePossibleKeyword()) + } + + override fun visit(element: CgLiteral) { + val value = with(element.value) { + when (this) { + is Double -> toStringConstant() + is String -> "\"" + escapeCharacters() + "\"" + else -> "$this" + } + } + print(value) + } + + private fun Double.toStringConstant() = when { + isNaN() -> "Number.NaN" + this == Double.POSITIVE_INFINITY -> "Number.POSITIVE_INFINITY" + this == Double.NEGATIVE_INFINITY -> "Number.NEGATIVE_INFINITY" + else -> "$this" + } + + override fun visit(element: CgStaticsRegion) { + if (element.content.isEmpty()) return + + print(regionStart) + element.header?.let { print(" $it") } + println() + + withIndent { + for (item in element.content) { + println() + item.accept(this) + } + } + + println(regionEnd) + } + + + override fun visit(element: CgTestClass) { + element.body.accept(this) + } + + override fun visit(element: CgFieldAccess) { + element.caller.accept(this) + renderAccess(element.caller) + print(element.fieldId.name) + } + + override fun visit(element: CgArrayElementAccess) { + element.array.accept(this) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgArrayAnnotationArgument) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgAnonymousFunction) { + print("function (") + element.parameters.renderSeparated(true) + println(") {") + // cannot use visit(element.body) here because { was already printed + withIndent { + for (statement in element.body) { + statement.accept(this) + } + } + print("}") + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + // TODO SEVERE + override fun visit(element: CgTypeCast) { + element.expression.accept(this) +// throw Exception("TypeCast not yet implemented") + } + + override fun visit(element: CgSpread) { + print("...") + element.array.accept(this) + } + + override fun visit(element: CgNotNullAssertion) { + throw UnsupportedOperationException("JavaScript does not support not null assertions") + } + + override fun visit(element: CgAllocateArray) { + print("new Array(${element.size})") + } + + override fun visit(element: CgAllocateInitializedArray) { + print("[") + element.initializer.accept(this) + print("]") + } + + // TODO SEVERE: I am unsure about this + override fun visit(element: CgArrayInitializer) { + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + + element.values.renderElements(elementsInLine) + } + + @Suppress("DuplicatedCode") + override fun visit(element: CgTestClassFile) { + context.collectedImports.filterIsInstance().forEach { + renderRegularImport(it) + } + println() + element.testClass.accept(this) + } + + override fun visit(element: CgSwitchCaseLabel) { + if (element.label != null) { + print("case ") + element.label!!.accept(this) + } else { + print("default") + } + println(": ") + visit(element.statements, printNextLine = true) + } + + @Suppress("DuplicatedCode") + override fun visit(element: CgSwitchCase) { + print("switch (") + element.value.accept(this) + println(") {") + withIndent { + for (caseLabel in element.labels) { + caseLabel.accept(this) + } + element.defaultLabel?.accept(this) + } + println("}") + } + + override fun visit(element: CgGetLength) { + element.variable.accept(this) + print(".size") + } + + override fun visit(element: CgGetJavaClass) { + throw UnsupportedOperationException("No Java classes in JavaScript") + } + + override fun visit(element: CgGetKotlinClass) { + throw UnsupportedOperationException("No Kotlin classes in JavaScript") + } + + override fun visit(element: CgConstructorCall) { + print("new $fileUnderTestAliases.${element.executableId.classId.name}") + print("(") + element.arguments.renderSeparated() + print(")") + } + + override fun renderRegularImport(regularImport: RegularImport) { + println("const ${regularImport.packageName} = require(\"${regularImport.className}\")") + } + + override fun renderStaticImport(staticImport: StaticImport) { + throw Exception("Not implemented yet") + } + + override fun renderMethodSignature(element: CgTestMethod) { + println("it(\"${element.name}\", function ()") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + println("it(\"${element.name}\", function ()") + + } + + override fun visit(element: CgMethod) { + super.visit(element) + if (element is CgTestMethod || element is CgErrorTestMethod) { + println(")") + } + } + + override fun visit(element: CgErrorTestMethod) { + renderMethodSignature(element) + visit(element as CgMethod) + } + + override fun visit(element: CgThrowStatement) { + // TODO: Should we render throw statement right here? + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + throw UnsupportedOperationException() + } + + override fun visit(element: CgNamedAnnotationArgument) { + + } + + override fun visit(element: CgMultipleArgsAnnotation) { + + } + + override fun visit(element: CgMethodCall) { + val caller = element.caller + if (caller != null) { + caller.accept(this) + renderAccess(caller) + } else { + val method = element.executableId + if (method is BuiltinMethodId) { + + } else if (method.isStatic) { + val line = if (method.classId.toString() == "undefined") "" else "${method.classId}." + print("$fileUnderTestAliases.$line") + } else { + print("$fileUnderTestAliases.") + } + } + print(element.executableId.name.escapeNamePossibleKeyword()) + renderTypeParameters(element.typeParameters) + if (element.type.name == "error") { + print("(") + element.arguments[0].accept(this@CgJsRenderer) + print(", ") + print("Error, ") + element.arguments[2].accept(this@CgJsRenderer) + print(")") + } else { + renderExecutableCallArguments(element) + } + } + + //TODO MINOR: check + override fun renderForLoopVarControl(element: CgForLoop) { + print("for (") + with(element.initialization) { + print("let ") + visit(variable) + print(" = ") + initializer?.accept(this@CgJsRenderer) + print("; ") + visit(element.condition) + print("; ") + print(element.update) + } + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + if (element.isMutable) print("var ") else print("let ") + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte) = "$byte" + + override fun toStringConstantImpl(short: Short) = "$short" + + override fun toStringConstantImpl(int: Int) = "$int" + + override fun toStringConstantImpl(long: Long) = "$long" + + override fun toStringConstantImpl(float: Float) = "$float" + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + //TODO MINOR: check + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + //TODO SEVERE: check + override fun renderExceptionCatchVariable(exception: CgVariable) { + print("${exception.name.escapeNamePossibleKeyword()}: ${exception.type}") + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = + if (isLanguageKeyword(s, context.codeGenLanguage)) "`$s`" else s + + //TODO MINOR: check + override fun String.escapeCharacters(): String = + StringEscapeUtils.escapeJava(this) + .replace("$", "\\$") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt b/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt new file mode 100644 index 0000000000..284d57aac7 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/JsFuzzer.kt @@ -0,0 +1,32 @@ +package fuzzer + +import fuzzer.providers.JsConstantsModelProvider +import fuzzer.providers.JsMultipleTypesModelProvider +import fuzzer.providers.JsObjectModelProvider +import fuzzer.providers.JsPrimitivesModelProvider +import fuzzer.providers.JsStringModelProvider +import fuzzer.providers.JsUndefinedModelProvider +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.fuzz + +object JsFuzzer { + + fun jsFuzzing( + modelProvider: (ModelProvider) -> ModelProvider = { it }, + methodUnderTestDescription: FuzzedMethodDescription + ): Sequence> { + val modelProviderWithFallback = modelProvider( + ModelProvider.of( + JsConstantsModelProvider, + JsUndefinedModelProvider, + JsStringModelProvider, + JsMultipleTypesModelProvider, + JsPrimitivesModelProvider, + JsObjectModelProvider, + ) + ) + return fuzz(methodUnderTestDescription, modelProviderWithFallback) + } +} diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt new file mode 100644 index 0000000000..768562c4ae --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsConstantsModelProvider.kt @@ -0,0 +1,61 @@ +package fuzzer.providers + +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.isJsPrimitive +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider + +object JsConstantsModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.concreteValues + .asSequence() + .filter { (classId, _) -> + (classId as JsClassId).isJsPrimitive + } + .forEach { (_, value, op) -> + sequenceOf( + JsPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, + modifyValue(value, op as FuzzedOp) + ) + .filterNotNull() + .forEach { m -> + description.parametersMap.getOrElse(m.model.classId) { + description.parametersMap.getOrElse(jsUndefinedClassId) { emptyList() } + }.forEach { index -> + yield(FuzzedParameter(index, m)) + } + } + } + } + + @Suppress("DuplicatedCode") + internal fun modifyValue(value: Any, op: FuzzedOp): FuzzedValue? { + if (!op.isComparisonOp()) return null + val multiplier = if (op == FuzzedOp.LT || op == FuzzedOp.GE) -1 else 1 + return when (value) { + is Boolean -> value.not() + is Byte -> value + multiplier.toByte() + is Char -> (value.code + multiplier).toChar() + is Short -> value + multiplier.toShort() + is Int -> value + multiplier + is Long -> value + multiplier.toLong() + is Float -> value + multiplier.toDouble() + is Double -> value + multiplier.toDouble() + else -> null + }?.let { + JsPrimitiveModel(it).fuzzed { + summary = "%var% ${ + (if (op == FuzzedOp.EQ || op == FuzzedOp.LE || op == FuzzedOp.GE) { + op.reverseOrNull() ?: error("cannot find reverse operation for $op") + } else op).sign + } $value" + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt new file mode 100644 index 0000000000..2c0f1af52a --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsMultipleTypesModelProvider.kt @@ -0,0 +1,77 @@ +package fuzzer.providers + +import fuzzer.providers.JsPrimitivesModelProvider.matchClassId +import fuzzer.providers.JsPrimitivesModelProvider.primitivesForString +import fuzzer.providers.JsStringModelProvider.mutate +import fuzzer.providers.JsStringModelProvider.random +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsMultipleClassId +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.isJsPrimitive +import org.utbot.framework.plugin.api.js.util.jsStringClassId +import org.utbot.framework.plugin.api.js.util.toJsClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.ModelProvider + +object JsMultipleTypesModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val parametersFiltered = description.parametersMap.filter { (classId, _) -> + classId is JsMultipleClassId + } + parametersFiltered.forEach { (jsMultipleClassId, indices) -> + val types = (jsMultipleClassId as JsMultipleClassId).types + types.forEach { classId -> + when { + classId.isJsPrimitive -> { + val concreteValuesFiltered = description.concreteValues.filter { (localClassId, _) -> + (localClassId as JsClassId).isJsPrimitive + } + concreteValuesFiltered.forEach { (_, value, op) -> + sequenceOf( + JsPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, + JsConstantsModelProvider.modifyValue(value, op as FuzzedOp) + ).filterNotNull() + .forEach { m -> + indices.forEach { index -> + yield(FuzzedParameter(index, m)) + } + } + } + matchClassId(classId).forEach { value -> + indices.forEach { index -> yield(FuzzedParameter(index, value)) } + } + } + + classId == jsStringClassId -> { + val concreteValuesFiltered = description.concreteValues + .asSequence() + .filter { (classId, _) -> classId.toJsClassId() == jsStringClassId } + concreteValuesFiltered.forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op as FuzzedOp)) + .asSequence() + .filterNotNull() + .map { JsPrimitiveModel(it) } + .forEach { m -> + indices.forEach { index -> + yield( + FuzzedParameter( + index, + m.fuzzed { summary = "%var% = string" } + ) + ) + } + } + } + primitivesForString().forEach { value -> + indices.forEach { index -> yield(FuzzedParameter(index, value)) } + } + } + + else -> throw IllegalStateException("Not yet implemented!") + } + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt new file mode 100644 index 0000000000..3b87fbfeb6 --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt @@ -0,0 +1,88 @@ +package fuzzer.providers + +import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.UtAssembleModel +import org.utbot.framework.plugin.api.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsConstructorId +import org.utbot.framework.plugin.api.js.util.isJsBasic +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import org.utbot.fuzzer.SimpleIdGenerator +import org.utbot.fuzzer.TooManyCombinationsException +import org.utbot.fuzzer.fuzz + +object JsObjectModelProvider : ModelProvider { + + val idGenerator = SimpleIdGenerator() + + private val primitiveModelProviders = ModelProvider.of( + JsConstantsModelProvider, + JsUndefinedModelProvider, + JsStringModelProvider, + JsMultipleTypesModelProvider, + JsPrimitivesModelProvider, + ) + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val fuzzedValues = with(description) { + parameters.asSequence() + .filterNot { (it as JsClassId).isJsBasic } + .map { classId -> + val constructor = (classId as JsClassId).allConstructors.first() as JsConstructorId + constructor + }.associateWith { constructor -> + fuzzParameters(constructor, primitiveModelProviders) + }.flatMap { (constructor, fuzzedParams) -> + fuzzedParams.map { params -> + assemble(idGenerator.asInt, constructor, params) + } + } + } + fuzzedValues.forEach { fuzzedValue -> + description.parametersMap[fuzzedValue.model.classId]?.forEach { index -> + yieldValue(index, fuzzedValue) + } + } + } + + private fun assemble(id: Int, constructor: ConstructorId, values: List): FuzzedValue { + val instantiationChain = mutableListOf() + val model = UtAssembleModel( + id, + constructor.classId, + "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), + instantiationChain = instantiationChain, + modificationsChain = mutableListOf() + ).apply { + instantiationChain += UtExecutableCallModel(null, constructor, values.map { it.model }, this) + }.fuzzed { + summary = + "%var% = ${constructor.classId.simpleName}(${constructor.parameters.joinToString { it.simpleName }})" + } + return model + } + + private fun FuzzedMethodDescription.fuzzParameters( + constructorId: ConstructorId, + vararg modelProviders: ModelProvider + ): Sequence> { + val fuzzedMethod = FuzzedMethodDescription( + executableId = constructorId, + concreteValues = this.concreteValues + ).apply { + this.packageName = this@fuzzParameters.packageName + } + return try { + fuzz(fuzzedMethod, *modelProviders) + } catch (t: TooManyCombinationsException) { + emptySequence() + } + } + + +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt new file mode 100644 index 0000000000..b168b3ae7b --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsPrimitivesModelProvider.kt @@ -0,0 +1,70 @@ +package fuzzer.providers + +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.jsBooleanClassId +import org.utbot.framework.plugin.api.js.util.jsDoubleClassId +import org.utbot.framework.plugin.api.js.util.jsNumberClassId +import org.utbot.framework.plugin.api.js.util.jsStringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue + +object JsPrimitivesModelProvider : ModelProvider { + + // TODO SEVERE: research overflows in js. For now these nums are low not to go beyond Long (will be fixed) + internal const val MAX_INT = 1024 + internal const val MIN_INT = -1024 + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.parametersMap.forEach { (classId, parameterIndices) -> + val primitives = matchClassId(classId as JsClassId) + primitives.forEach { model -> + parameterIndices.forEach { index -> + yieldValue(index, model) + } + } + } + } + + internal fun matchClassId(classId: JsClassId): List { + val fuzzedValues = when (classId) { + jsBooleanClassId -> listOf( + JsPrimitiveModel(false).fuzzed { summary = "%var% = false" }, + JsPrimitiveModel(true).fuzzed { summary = "%var% = true" } + ) + + jsNumberClassId -> listOf( + JsPrimitiveModel(0).fuzzed { summary = "%var% = 0" }, + JsPrimitiveModel(1).fuzzed { summary = "%var% > 0" }, + JsPrimitiveModel((-1)).fuzzed { summary = "%var% < 0" }, + JsPrimitiveModel(MIN_INT).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + JsPrimitiveModel(MAX_INT).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, + ) + + jsDoubleClassId -> listOf( + JsPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" }, + JsPrimitiveModel(1.1).fuzzed { summary = "%var% > 0.0" }, + JsPrimitiveModel(-1.1).fuzzed { summary = "%var% < 0.0" }, + JsPrimitiveModel(MIN_INT.toDouble()).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + JsPrimitiveModel(MAX_INT.toDouble()).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, +// TODO SEVERE: Think about such values as they are present in JavaScript. +// UtPrimitiveModel(Double.NEGATIVE_INFINITY).fuzzed { summary = "%var% = Double.NEGATIVE_INFINITY" }, +// UtPrimitiveModel(Double.POSITIVE_INFINITY).fuzzed { summary = "%var% = Double.POSITIVE_INFINITY" }, +// JsPrimitiveModel(Double.NaN).fuzzed { summary = "%var% = Double.NaN" }, + ) + + jsStringClassId -> primitivesForString() + else -> listOf() + } + return fuzzedValues + } + + internal fun primitivesForString() = listOf( + JsPrimitiveModel("").fuzzed { summary = "%var% = empty string" }, + JsPrimitiveModel(" ").fuzzed { summary = "%var% = blank string" }, + JsPrimitiveModel("string").fuzzed { summary = "%var% != empty string" }, + ) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt new file mode 100644 index 0000000000..9c08e0496c --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsStringModelProvider.kt @@ -0,0 +1,53 @@ +package fuzzer.providers + +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.jsStringClassId +import org.utbot.framework.plugin.api.js.util.toJsClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.ModelProvider +import kotlin.random.Random + +object JsStringModelProvider : ModelProvider { + + internal val random = Random(72923L) + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + description.concreteValues + .asSequence() + .filter { (classId, _) -> classId.toJsClassId() == jsStringClassId } + .forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op as FuzzedOp)) + .asSequence() + .filterNotNull() + .map { JsPrimitiveModel(it) }.forEach { model -> + description.parametersMap.keys.indices.forEach { index -> + yield(FuzzedParameter(index, model.fuzzed { summary = "%var% = string" })) + } + } + } + } + + fun mutate(random: Random, value: String?, op: FuzzedOp): String? { + if (value.isNullOrEmpty() || op != FuzzedOp.CH) return null + val indexOfMutation = random.nextInt(value.length) + return value.replaceRange( + indexOfMutation, + indexOfMutation + 1, + SingleCharacterSequence(value[indexOfMutation] - random.nextInt(1, 128)) + ) + } + + private class SingleCharacterSequence(private val character: Char) : CharSequence { + override val length: Int + get() = 1 + + override fun get(index: Int): Char = character + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + throw UnsupportedOperationException() + } + + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt new file mode 100644 index 0000000000..c31c29c4ce --- /dev/null +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsUndefinedModelProvider.kt @@ -0,0 +1,39 @@ +package fuzzer.providers + +import fuzzer.providers.JsPrimitivesModelProvider.MAX_INT +import fuzzer.providers.JsPrimitivesModelProvider.MIN_INT +import org.utbot.framework.plugin.api.js.JsPrimitiveModel +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider + +object JsUndefinedModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription): Sequence = sequence { + val parameters = description.parametersMap.getOrDefault(jsUndefinedClassId, emptyList()) + val primitives: List = generateValues() + primitives.forEach { model -> + parameters.forEach { index -> + yield(FuzzedParameter(index, model)) + } + } + } + + private fun generateValues() = + listOf( + JsPrimitiveModel(false).fuzzed { summary = "%var% = false" }, + JsPrimitiveModel(true).fuzzed { summary = "%var% = false" }, + + JsPrimitiveModel(0).fuzzed { summary = "%var% = 0" }, + JsPrimitiveModel(-1).fuzzed { summary = "%var% < 0" }, + JsPrimitiveModel(1).fuzzed { summary = "%var% > 0" }, + JsPrimitiveModel(MAX_INT).fuzzed { summary = "%var% = Number.MAX_SAFE_VALUE" }, + JsPrimitiveModel(MIN_INT).fuzzed { summary = "%var% = Number.MIN_SAFE_VALUE" }, + + JsPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" }, + JsPrimitiveModel(-1.0).fuzzed { summary = "%var% < 0.0" }, + JsPrimitiveModel(1.0).fuzzed { summary = "%var% > 0.0" }, + ) +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt new file mode 100644 index 0000000000..b6e385b5d8 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsClassAstVisitor.kt @@ -0,0 +1,26 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsClassAstVisitor( + private val target: String? +) : NodeVisitor(LexicalContext()) { + + lateinit var targetClassNode: ClassNode + lateinit var atLeastSomeClassNode: ClassNode + var classNodesCount = 0 + + override fun enterClassNode(classNode: ClassNode?): Boolean { + classNode?.let { + classNodesCount++ + atLeastSomeClassNode = it + if (it.ident.name.toString() == target) { + targetClassNode = it + return false + } + } + return true + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt new file mode 100644 index 0000000000..a7dc3a7b27 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFunctionAstVisitor.kt @@ -0,0 +1,32 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsFunctionAstVisitor( + private val target: String, + private val className: String? +) : NodeVisitor(LexicalContext()) { + + private var lastVisitedClassName: String = "" + lateinit var targetFunctionNode: FunctionNode + + override fun enterClassNode(classNode: ClassNode?): Boolean { + classNode?.let { + lastVisitedClassName = it.ident.name.toString() + } + return true + } + + override fun enterFunctionNode(functionNode: FunctionNode?): Boolean { + functionNode?.let { + if (it.name.toString() == target && (className ?: "") == lastVisitedClassName) { + targetFunctionNode = it + return false + } + } + return true + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt new file mode 100644 index 0000000000..c108dbd995 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsFuzzerAstVisitor.kt @@ -0,0 +1,76 @@ +package parser + +import com.oracle.js.parser.ir.BinaryNode +import com.oracle.js.parser.ir.CaseNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.LiteralNode +import com.oracle.js.parser.ir.Node +import com.oracle.js.parser.ir.visitor.NodeVisitor +import com.oracle.truffle.api.strings.TruffleString +import org.utbot.framework.plugin.api.js.util.jsBooleanClassId +import org.utbot.framework.plugin.api.js.util.jsNumberClassId +import org.utbot.framework.plugin.api.js.util.jsStringClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedOp + +class JsFuzzerAstVisitor : NodeVisitor(LexicalContext()) { + private var lastFuzzedOpGlobal = FuzzedOp.NONE + + val fuzzedConcreteValues = mutableSetOf() + override fun enterCaseNode(caseNode: CaseNode?): Boolean { + caseNode?.test?.let { + validateNode(it) + } + return true + } + + override fun enterBinaryNode(binaryNode: BinaryNode?): Boolean { + binaryNode?.let { binNode -> + val compOp = """>=|<=|>|<|==|!=""".toRegex() + val curOp = compOp.find(binNode.toString())?.value + val currentFuzzedOp = FuzzedOp.values().find { curOp == it.sign } ?: FuzzedOp.NONE + lastFuzzedOpGlobal = currentFuzzedOp + validateNode(binNode.lhs) + lastFuzzedOpGlobal = lastFuzzedOpGlobal.reverseOrElse { FuzzedOp.NONE } + validateNode(binNode.rhs) + } + return true + } + + private fun validateNode(literalNode: Node) { + if (literalNode !is LiteralNode<*>) return + when (literalNode.value) { + is TruffleString -> { + fuzzedConcreteValues.add( + FuzzedConcreteValue( + jsStringClassId, + literalNode.value.toString(), + lastFuzzedOpGlobal + ) + ) + } + + is Boolean -> { + fuzzedConcreteValues.add( + FuzzedConcreteValue( + jsBooleanClassId, + literalNode.value, + lastFuzzedOpGlobal + ) + ) + } + + is Int -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + + is Long -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + + is Double -> { + fuzzedConcreteValues.add(FuzzedConcreteValue(jsNumberClassId, literalNode.value, lastFuzzedOpGlobal)) + } + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsParserUtils.kt b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt new file mode 100644 index 0000000000..a8715720d1 --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsParserUtils.kt @@ -0,0 +1,29 @@ +package parser + +import com.oracle.js.parser.ErrorManager +import com.oracle.js.parser.Parser +import com.oracle.js.parser.ScriptEnvironment +import com.oracle.js.parser.Source +import com.oracle.js.parser.ir.ClassNode + +object JsParserUtils { + + // TODO SEVERE: function only works in the same file scope. Add search in exports. + fun searchForClassDecl(className: String?, fileText: String, strict: Boolean = false): ClassNode? { + val parser = Parser( + ScriptEnvironment.builder().build(), + Source.sourceFor("jsFile", fileText), + ErrorManager.ThrowErrorManager() + ) + val fileNode = parser.parse() + val visitor = JsClassAstVisitor(className) + fileNode.accept(visitor) + return try { + visitor.targetClassNode + } catch (e: Exception) { + if (!strict && visitor.classNodesCount == 1) { + visitor.atLeastSomeClassNode + } else null + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt new file mode 100644 index 0000000000..e77db964fa --- /dev/null +++ b/utbot-js/src/main/kotlin/parser/JsToplevelFunctionAstVisitor.kt @@ -0,0 +1,22 @@ +package parser + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import com.oracle.js.parser.ir.LexicalContext +import com.oracle.js.parser.ir.visitor.NodeVisitor + +class JsToplevelFunctionAstVisitor : NodeVisitor(LexicalContext()) { + + val extractedMethods = mutableListOf() + + override fun enterClassNode(classNode: ClassNode?): Boolean { + return false + } + + override fun enterFunctionNode(functionNode: FunctionNode?): Boolean { + functionNode?.let { + extractedMethods += it + } + return false + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/CoverageService.kt b/utbot-js/src/main/kotlin/service/CoverageService.kt new file mode 100644 index 0000000000..8728ea3292 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/CoverageService.kt @@ -0,0 +1,98 @@ +package service + +import org.apache.commons.io.FileUtils +import org.json.JSONException +import org.json.JSONObject +import org.utbot.framework.plugin.api.TimeoutException +import utils.JsCmdExec +import java.io.File +import java.util.Collections + +class CoverageService( + private val context: ServiceContext, + private val scriptText: String, + private val id: Int, + private val originalFileName: String, + private val newFileName: String, + private val basicCoverage: List = emptyList(), + val errors: MutableList +) { + init { + with(context) { + generateCoverageReport(projectPath, filePathToInference) + } + } + + fun getCoveredLines(): List { + if (id in errors) return emptyList() + val jsonText = with(context) { + val file = + File("$projectPath${File.separator}$utbotDir${File.separator}coverage$id${File.separator}coverage-final.json") + file.readText() + } + val json = JSONObject(jsonText) + try { + val neededKey = json.keySet().find { it.contains(originalFileName) } + json.getJSONObject(neededKey) + val coveredStatements = json + .getJSONObject(neededKey) + .getJSONObject("s") + val result = coveredStatements.keySet().flatMap { + val count = coveredStatements.getInt(it) + Collections.nCopies(count, it.toInt()) + }.toMutableList() + basicCoverage.forEach { + result.remove(it) + } + return result + } catch (e: JSONException) { + return emptyList() + } + } + + fun removeTempFiles() { + with(context) { + FileUtils.deleteDirectory(File("$projectPath${File.separator}$utbotDir${File.separator}coverage$id")) + } + } + + private fun generateCoverageReport(workingDir: String, filePath: String) { + val dir = File("$workingDir${File.separator}${context.utbotDir}${File.separator}coverage$id") + .also { it.mkdir() } + try { + val (_, error) = when (originalFileName) { + newFileName -> { + JsCmdExec.runCommand( + cmd = "nyc " + + "--report-dir=${dir.absolutePath} " + + "--reporter=\"json\" " + + "--temp-dir=${dir.absolutePath}${File.separator}cache$id " + + "node $filePath", + shouldWait = true, + dir = workingDir, + timeout = context.nodeTimeout, + ) + } + + else -> { + JsCmdExec.runCommand( + cmd = "nyc " + + "--report-dir=${dir.absolutePath} " + + "--reporter=\"json\" " + + "--temp-dir=${dir.absolutePath}${File.separator}cache$id " + + "node -e \"$scriptText\" ", + shouldWait = true, + dir = File(filePath).parent, + timeout = context.nodeTimeout, + ) + } + } + val errText = error.readText() + if (errText.isNotEmpty()) { + println(errText) + } + } catch (e: TimeoutException) { + errors += id + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/ServiceContext.kt b/utbot-js/src/main/kotlin/service/ServiceContext.kt new file mode 100644 index 0000000000..2a3fa6d3ff --- /dev/null +++ b/utbot-js/src/main/kotlin/service/ServiceContext.kt @@ -0,0 +1,10 @@ +package service + +data class ServiceContext( + val utbotDir: String, + val projectPath: String, + val filePathToInference: String, + val trimmedFileText: String, + val fileText: String? = null, + val nodeTimeout: Long, +) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/service/TernService.kt b/utbot-js/src/main/kotlin/service/TernService.kt new file mode 100644 index 0000000000..8a2796e632 --- /dev/null +++ b/utbot-js/src/main/kotlin/service/TernService.kt @@ -0,0 +1,210 @@ +package service + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import org.json.JSONException +import org.json.JSONObject +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsMultipleClassId +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId +import parser.JsParserUtils +import utils.JsCmdExec +import utils.MethodTypes +import utils.constructClass +import java.io.File +import java.nio.charset.Charset +import java.util.Locale + +/* + NOTE: this approach is quite bad, but we failed to implement alternatives. + TODO: 1. MINOR: Find a better solution after the first stable version. + 2. SEVERE: Load all necessary .js files in Tern.js since functions can be exported and used in other files. + */ + +/** + * Installs and sets up scripts for running Tern.js type guesser. + */ +class TernService(val context: ServiceContext) { + + + private fun ternScriptCode() = """ +const tern = require("tern/lib/tern") +const condense = require("tern/lib/condense.js") +const util = require("tern/test/util.js") +const fs = require("fs") +const path = require("path") + +var condenseDir = ""; + +function runTest(options) { + + var server = new tern.Server({ + projectDir: util.resolve(condenseDir), + defs: [util.ecmascript], + plugins: options.plugins, + getFile: function(name) { + return fs.readFileSync(path.resolve(condenseDir, name), "utf8"); + } + }); + options.load.forEach(function(file) { + server.addFile(file) + }); + server.flush(function() { + var origins = options.include || options.load; + var condensed = condense.condense(origins, null, {sortOutput: true}); + var out = JSON.stringify(condensed, null, 2); + console.log(out) + }); +} + +function test(options) { + if (typeof options == "string") options = {load: [options]}; + runTest(options); +} + +test("${context.filePathToInference}") + """ + + init { + run() + } + + private lateinit var json: JSONObject + + private fun run() { + with(context) { + setupTernEnv("$projectPath${File.separator}$utbotDir") + installDeps("$projectPath${File.separator}$utbotDir") + runTypeInferencer() + } + } + + private fun installDeps(path: String) { + JsCmdExec.runCommand( + cmd = "npm install tern -l", + dir = path, + ) + } + + private fun setupTernEnv(path: String) { + File(path).mkdirs() + val ternScriptFile = File("$path${File.separator}ternScript.js") + ternScriptFile.writeText(ternScriptCode(), Charset.defaultCharset()) + } + + private fun runTypeInferencer() { + with(context) { + val (reader, _) = JsCmdExec.runCommand( + cmd = "node ${projectPath}${File.separator}$utbotDir${File.separator}ternScript.js", + dir = "$projectPath${File.separator}$utbotDir${File.separator}", + shouldWait = true, + timeout = 20 + ) + val text = reader.readText().replaceAfterLast("}", "") + json = try { + JSONObject(text) + } catch (_: Throwable) { + JSONObject() + } + } + } + + fun processConstructor(classNode: ClassNode): List { + return try { + val classJson = json.getJSONObject(classNode.ident.name.toString()) + val constructorFunc = classJson.getString("!type") + .filterNot { setOf(' ', '+', '!').contains(it) } + extractParameters(constructorFunc) + } catch (e: JSONException) { + (classNode.constructor.value as FunctionNode).parameters.map { jsUndefinedClassId } + } + } + + private fun extractParameters(line: String): List { + val parametersRegex = Regex("fn[(](.+)[)]") + return parametersRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + val paramList = value.split(',') + paramList.map { param -> + val paramReg = Regex(":(.*)") + try { + makeClassId( + paramReg.find(param)?.groups?.get(1)?.value + ?: throw IllegalStateException() + ) + } catch (t: Throwable) { + jsUndefinedClassId + } + } + } ?: emptyList() + } + + private fun extractReturnType(line: String): JsClassId { + val returnTypeRegex = Regex("->(.*)") + return returnTypeRegex.find(line)?.groups?.get(1)?.let { matchResult -> + val value = matchResult.value + try { + makeClassId(value) + } catch (t: Throwable) { + jsUndefinedClassId + } + } ?: jsUndefinedClassId + } + + fun processMethod(className: String?, funcNode: FunctionNode, isToplevel: Boolean = false): MethodTypes { + // Js doesn't support nested classes, so if the function is not top-level, then we can check for only one parent class. + try { + var scope = className?.let { + if (!isToplevel) json.getJSONObject(it) else json + } ?: json + try { + scope.getJSONObject(funcNode.name.toString()) + } catch (e: JSONException) { + scope = scope.getJSONObject("prototype") + } + val methodJson = scope.getJSONObject(funcNode.name.toString()) + val typesString = methodJson.getString("!type") + .filterNot { setOf(' ', '+', '!').contains(it) } + val parametersList = lazy { extractParameters(typesString) } + val returnType = lazy { extractReturnType(typesString) } + + return MethodTypes(parametersList, returnType) + } catch (e: Exception) { + return MethodTypes( + lazy { funcNode.parameters.map { jsUndefinedClassId } }, + lazy { jsUndefinedClassId } + ) + } + } + + //TODO MINOR: move to appropriate place (JsIdUtil or JsClassId constructor) + private fun makeClassId(name: String): JsClassId { + val classId = when { + // TODO SEVERE: I don't know why Tern sometimes says that type is "0" + name == "?" || name.toIntOrNull() != null -> jsUndefinedClassId + Regex("\\[(.*)]").matches(name) -> { + val arrType = Regex("\\[(.*)]").find(name)?.groups?.get(1)?.value ?: throw IllegalStateException() + JsClassId( + jsName = "array", + elementClassId = makeClassId(arrType) + ) + } + + name.contains('|') -> JsMultipleClassId(name.lowercase(Locale.getDefault())) + else -> JsClassId(name.lowercase(Locale.getDefault())) + } + + return try { + val classNode = JsParserUtils.searchForClassDecl( + className = name, + fileText = context.fileText ?: context.trimmedFileText, + strict = true, + ) + classNode?.let { + JsClassId(name).constructClass(this, it) + } ?: classId + } catch (e: Exception) { + classId + } + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt new file mode 100644 index 0000000000..4a83135dc8 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsExportsSettings.kt @@ -0,0 +1,11 @@ +package settings + +object JsExportsSettings { + + // Anchors for exports in users code. Used in regexes to modify this section on demand. + const val startComment = "// Start of exports generated by UTBot" + const val endComment = "// End of exports generated by UTBot" + + // May change based on whether user's file is inside JavaScript module or not. + const val exportsLinePrefix = "module.exports = " +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt new file mode 100644 index 0000000000..91590b0c87 --- /dev/null +++ b/utbot-js/src/main/kotlin/settings/JsTestGenerationSettings.kt @@ -0,0 +1,19 @@ +package settings + +object JsTestGenerationSettings { + + // Used for toplevel functions in IDEA plugin. + const val dummyClassName = "toplevelHack" + + // Default timeout for Node.js to try run a single testcase. + const val defaultTimeout = 15L + + // Name of file under test when importing it. + const val fileUnderTestAliases = "fileUnderTest" + + // Anchor for obtaining function under test call results. Used in regexes. + const val functionCallResultAnchor = "Utbot result:" + + // Name of temporary files created. + const val tempFileName = "temp" +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt new file mode 100644 index 0000000000..8909c3ff3b --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsClassConstructors.kt @@ -0,0 +1,74 @@ +package utils + +import com.oracle.js.parser.ir.ClassNode +import com.oracle.js.parser.ir.FunctionNode +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.JsConstructorId +import org.utbot.framework.plugin.api.js.JsMethodId +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId +import service.TernService + +fun JsClassId.constructClass( + ternService: TernService, + classNode: ClassNode? = null, + functions: List = emptyList() +): JsClassId { + val className = classNode?.ident?.name?.toString() + val methods = constructMethods(classNode, ternService, className, functions) + + val constructor = classNode?.let { + JsConstructorId( + JsClassId(name), + ternService.processConstructor(it), + ) + } + val newClassId = JsClassId( + jsName = name, + methods = methods, + constructor = constructor, + classPackagePath = ternService.context.projectPath, + classFilePath = ternService.context.filePathToInference, + ) + methods.forEach { + it.classId = newClassId + } + constructor?.classId = newClassId + return newClassId +} + +private fun JsClassId.constructMethods( + classNode: ClassNode?, + ternService: TernService, + className: String?, + functions: List +): Sequence { + with(this) { + val methods = classNode?.classElements?.map { + val funcNode = it.value as FunctionNode + val types = ternService.processMethod(className, funcNode) + JsMethodId( + classId = JsClassId(name), + name = funcNode.name.toString(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = it.isStatic, + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }?.asSequence() ?: + // used for toplevel functions + functions.map { funcNode -> + val types = ternService.processMethod(className, funcNode, true) + JsMethodId( + classId = JsClassId(name), + name = funcNode.name.toString(), + returnTypeNotLazy = jsUndefinedClassId, + parametersNotLazy = emptyList(), + staticModifier = true, + lazyReturnType = types.returnType, + lazyParameters = types.parameters, + ) + }.asSequence() + return methods + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/JsCmdExec.kt b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt new file mode 100644 index 0000000000..ed44323013 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/JsCmdExec.kt @@ -0,0 +1,40 @@ +package utils + +import org.utbot.framework.plugin.api.TimeoutException +import settings.JsTestGenerationSettings.defaultTimeout +import java.io.BufferedReader +import java.io.File +import java.util.Locale +import java.util.concurrent.TimeUnit + +object JsCmdExec { + + private val cmdPrefix = + if (System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows")) + "cmd.exe" else "/bin/bash" + private val cmdDelim = if (System.getProperty("os.name").lowercase(Locale.getDefault()).contains("windows")) + "/c" else "-c" + + fun runCommand( + cmd: String, + dir: String? = null, + shouldWait: Boolean = false, + timeout: Long = defaultTimeout + ): Pair { + val builder = ProcessBuilder(cmdPrefix, cmdDelim, cmd) + dir?.let { + builder.directory(File(it)) + } + val process = builder.start() + if (shouldWait) { + if (!process.waitFor(timeout, TimeUnit.SECONDS)) { + process.descendants().forEach { + it.destroy() + } + process.destroy() + throw TimeoutException("") + } + } + return process.inputStream.bufferedReader() to process.errorStream.bufferedReader() + } +} \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/MethodTypes.kt b/utbot-js/src/main/kotlin/utils/MethodTypes.kt new file mode 100644 index 0000000000..0db8f962fd --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/MethodTypes.kt @@ -0,0 +1,8 @@ +package utils + +import org.utbot.framework.plugin.api.js.JsClassId + +data class MethodTypes( + val parameters: Lazy>, + val returnType: Lazy, +) \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/utils/PathResolver.kt b/utbot-js/src/main/kotlin/utils/PathResolver.kt new file mode 100644 index 0000000000..8e1730db97 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/PathResolver.kt @@ -0,0 +1,12 @@ +package utils + +import java.nio.file.Paths + +object PathResolver { + + fun getRelativePath(to: String, from: String): String { + val toPath = Paths.get(to) + val fromPath = Paths.get(from) + return toPath.relativize(fromPath).toString().replace("\\", "/") + } +} diff --git a/utbot-js/src/main/kotlin/utils/ValueUtil.kt b/utbot-js/src/main/kotlin/utils/ValueUtil.kt new file mode 100644 index 0000000000..dacfa3dc80 --- /dev/null +++ b/utbot-js/src/main/kotlin/utils/ValueUtil.kt @@ -0,0 +1,45 @@ +package utils + +import org.json.JSONException +import org.json.JSONObject +import org.utbot.framework.plugin.api.js.JsClassId +import org.utbot.framework.plugin.api.js.util.jsBooleanClassId +import org.utbot.framework.plugin.api.js.util.jsErrorClassId +import org.utbot.framework.plugin.api.js.util.jsNumberClassId +import org.utbot.framework.plugin.api.js.util.jsStringClassId +import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId + +fun String.toJsAny(returnType: JsClassId): Pair { + return when { + this == "true" || this == "false" -> toBoolean() to jsBooleanClassId + this == "null" || this == "undefined" -> null to jsUndefinedClassId + Regex("^.*Error:.*").matches(this) -> this.replace("Error: ", "") to jsErrorClassId + Regex("\".*\"").matches(this) -> this.replace("\"", "") to jsStringClassId + else -> { + if (contains('.')) { + (toDoubleOrNull() ?: toBigDecimal()) to jsNumberClassId + } else { + val value = toByteOrNull() ?: toShortOrNull() ?: toIntOrNull() ?: toLongOrNull() + ?: toBigIntegerOrNull() ?: toDoubleOrNull() + if (value != null) value to jsNumberClassId else { + val obj = makeObject(this) + if (obj != null) obj to returnType else throw IllegalStateException() + } + } + } + } +} + +private fun makeObject(objString: String): Map? { + return try { + val trimmed = objString.substringAfter(" ") + val json = JSONObject(trimmed) + val resMap = mutableMapOf() + json.keySet().forEach { + resMap[it] = json.get(it).toString().toJsAny(jsUndefinedClassId).first as Any + } + resMap + } catch (e: JSONException) { + null + } +} \ No newline at end of file diff --git a/utbot-python/README.md b/utbot-python/README.md new file mode 100644 index 0000000000..e5c0f6529b --- /dev/null +++ b/utbot-python/README.md @@ -0,0 +1,48 @@ +# UTBot for Python + +UTBot is the tool for automated unit test generation. You can read more about this project [on the official website](https://www.utbot.org/). + +This is the support of UTBot for Python. + +UTBot tries to maximize the code coverage while minimizing the number of tests. For now, we use only the fuzzing technique for Python. + +# Get started + +There are two ways to use UTBot: as an IntelliJ IDEA plugin or through a command line interface. + +You can download both archives [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2956160534). + +## Python requirements + +UTBot Python has been tested on Python 3.8 and 3.9. Some syntax from Python 3.10 is not supported. + +Usually nothing has to be done manually, but if you have any troubles with requirements, refer to [requirements section](docs/CLI.md#requirements) in CLI documentation. + +## IntelliJ IDEA plugin + +IntelliJ IDEA version should be 2022.1. + +1. Make sure you already have the Python plugin installed. + +2. Download the archive with the plugin and install it following [this instruction](https://www.jetbrains.com/help/idea/managing-plugins.html#install_plugin_from_disk). + +3. Configure the Python interpreter for your project and make sure that IDEA resolves all imports. + +4. After indexing has finished, move the cursor to a function, press ALT+SHIFT+U (or ALT+U, ALT+T in Ubuntu), and generate tests. + +### Dependency + +Package `com.intellij.modules.python` in `/utbot-intellij/resources/plugin.xml` is necessary dependecy for now, it is needed to use Python Psi tree. + + +## Command line interface + +You can find documentation on CLI usage [here](docs/CLI.md). + +# Contribute + +Read more in [UTBot Java Readme](../README.md#contribute-to-utbot-java). + +# Support + +Read more in [UTBot Java Readme](../README.md#find-support). diff --git a/utbot-python/build.gradle.kts b/utbot-python/build.gradle.kts new file mode 100644 index 0000000000..e4ad41ed9a --- /dev/null +++ b/utbot-python/build.gradle.kts @@ -0,0 +1,42 @@ +val sootCommitHash:String by rootProject +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + test { + useJUnitPlatform() + } +} + +dependencies { + api(project(":utbot-fuzzers")) + api(project(":utbot-framework")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation(group = "org.apache.commons", name = "commons-lang3", version = "3.12.0") + implementation(group = "io.github.danielnaczo", name = "python3parser", version = "1.0.4") + implementation(group = "commons-io", name = "commons-io", version = "2.11.0") + implementation("com.beust:klaxon:5.5") + implementation("com.squareup.moshi:moshi:1.11.0") + implementation("com.squareup.moshi:moshi-kotlin:1.11.0") + implementation("com.squareup.moshi:moshi-adapters:1.11.0") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation("org.functionaljava:functionaljava:5.0") + implementation("org.functionaljava:functionaljava-quickcheck:5.0") + implementation("org.functionaljava:functionaljava-java-core:5.0") + implementation(group = "org.apache.commons", name = "commons-text", version = apacheCommonsTextVersion) + implementation ("com.github.UnitTestBot:soot:${sootCommitHash}") +} \ No newline at end of file diff --git a/utbot-python/docs/CLI.md b/utbot-python/docs/CLI.md new file mode 100644 index 0000000000..bdc5c5e0b0 --- /dev/null +++ b/utbot-python/docs/CLI.md @@ -0,0 +1,101 @@ +## Build + +.jar file can be built in Github Actions with script `publish-plugin-and-cli-from-branch`. + +## Requirements + + - Required Java version: 11. + + - Prefered Python version: 3.8 or 3.9. + + Make sure that your Python has `pip` installed (this is usually the case). [Read more about pip installation](https://pip.pypa.io/en/stable/installation/). + + Before running utbot install pip requirements (or use `--install-requirements` flag in `generate_python` command): + + python -m pip install mypy==0.971 astor typeshed-client coverage + +## Basic usage + +Generate tests: + + java -jar utbot-cli.jar generate_python dir/file_with_sources.py -p -o generated_tests.py -s dir + +This will generate tests for top-level functions from `file_with_sources.py`. + +Run generated tests: + + java -jar utbot-cli.jar run_python generated_tests.py -p + +### `generate_python` options + +- `-s, --sys-path ,` + + (required) Directories to add to `sys.path`. One of directories must contain the file with the methods under test. + + `sys.path` is a list of strings that specifies the search path for modules. It must include paths for all user modules that are used in imports. + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `-o, --output ` + + (required) File for generated tests. + +- `--coverage ` + + File to write coverage report. + +- `-c, --class ` + + Specify top-level (ordinary, not nested) class under test. Without this option tests will be generated for top-level functions. + +- `-m, --methods ,` + + Specify methods under test. + +- `--install-requirements` + + Install Python requirements if missing. + +- `--do-not-minimize` + + Turn off minimization of the number of generated tests. + +- `--do-not-check-requirements` + + Turn off Python requirements check (to speed up). + +- `--visit-only-specified-source` + + Do not search for classes and imported modules in other Python files from `--sys-path` option. + +- `-t, --timeout INT` + + Specify the maximum time in milliseconds to spend on generating tests (60000 by default). + +- `--timeout-for-run INT` + + Specify the maximum time in milliseconds to spend on one function run (2000 by default). + +- `--test-framework [pytest|Unittest]` + + Test framework to be used. + +### `run_python` options + +- `-p, --python-path ` + + (required) Path to Python interpreter. + +- `--test-framework [pytest|Unittest]` + + Test framework of tests to run. + +- `-o, --output ` + + Specify file for report. + +## Problems + +- Unittest can not run tests from parent directories diff --git a/utbot-python/docs/docs.md b/utbot-python/docs/docs.md new file mode 100644 index 0000000000..96e0bce318 --- /dev/null +++ b/utbot-python/docs/docs.md @@ -0,0 +1,54 @@ +# UtBot-Python +__Task__: implement utbot for Python using fuzzing to generate tests. + +Subtasks: +* Get list of functions to be tested +* Generate input parameters for this functions +* Compute return values for this parameters +* Render tests + +## Getting list of functions + +We get list of functions to be tested from Intellij IDEA plugin. Other information we get from source code. + +Information about functions: +* Name +* List of parameters +* Source code +* Declaration file +* Type annotations for parameters and return type (optional) + +## Input parameters generation + +### Problem + +If we do not have type annotation, we have to find suitable types for this parameter. + +### Solution +Gather information about Python built-in types (by 'built-in types' we mean types that are implemented in C): + +* Name +* Methods: name + parameters (+ annotations) +* How to generate instances of this type (default, random, using constants from code) + +We can use CPython code and tests for it to gather this. + +For user class we need to initialize its fields recursively. Possible problems: getting types of fields, dynamic addition of new fields. + +To find suitable types for parameter we can look for them only in given and imported files. + +To narrow down the search of suitable types we can gather constraints for function parameters. For that we can analyze AST to see which attributes of parameter are used. + +## Run function with generated parameters + +After generating parameters for fuzzing we pass them on into the function under test and run it in a separate process. This approach is called concrete execution. + +To run the function we need to generate code that imports and calls it and saves result. + +## Get return value + +We write serialized return value in file. To serialize values of the most used built-in types we can use json module. For other types we will have to do it manually. + +## Test generation + +First we build AST of test code and then render it. diff --git a/utbot-python/samples/.gitignore b/utbot-python/samples/.gitignore new file mode 100644 index 0000000000..6ebbb99482 --- /dev/null +++ b/utbot-python/samples/.gitignore @@ -0,0 +1,3 @@ +.tmp/ +utbot_tests/ +utbot-cli.jar diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py b/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py new file mode 100644 index 0000000000..fe6ecb2585 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__arithmetic.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('samples') +import builtins +import arithmetic +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable arithmetic.calculate_function_value + # region + def test_calculate_function_value(self): + actual = arithmetic.calculate_function_value(1, 101) + + self.assertEqual(11886.327847992769, actual) + + def test_calculate_function_value1(self): + actual = arithmetic.calculate_function_value(4294967296, 101) + + self.assertEqual(65535.99845886229, actual) + + def test_calculate_function_value2(self): + actual = arithmetic.calculate_function_value(float('nan'), 4294967296) + + self.assertTrue(isinstance(actual, builtins.float)) + + def test_calculate_function_value_throws_t(self): + arithmetic.calculate_function_value(0, 101) + + # raises builtins.ZeroDivisionError + + def test_calculate_function_value_throws_t1(self): + arithmetic.calculate_function_value(101, 101) + + # raises builtins.ValueError + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py b/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py new file mode 100644 index 0000000000..3f04ba5a47 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__deep_equals.py @@ -0,0 +1,87 @@ +import sys +sys.path.append('samples') +import builtins +import deep_equals +import copyreg +import types +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable deep_equals.comparable_list + # region + def test_comparable_list(self): + actual = deep_equals.comparable_list(4294967296) + + comparable_class = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class.x = 0 + comparable_class1 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class1.x = 1 + comparable_class2 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class2.x = 2 + comparable_class3 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class3.x = 3 + comparable_class4 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class4.x = 4 + comparable_class5 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class5.x = 5 + comparable_class6 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class6.x = 6 + comparable_class7 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class7.x = 7 + comparable_class8 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class8.x = 8 + comparable_class9 = copyreg._reconstructor(deep_equals.ComparableClass, builtins.object, None) + comparable_class9.x = 9 + + self.assertEqual([comparable_class, comparable_class1, comparable_class2, comparable_class3, comparable_class4, comparable_class5, comparable_class6, comparable_class7, comparable_class8, comparable_class9], actual) + + # endregion + + # endregion + + # region Test suites for executable deep_equals.incomparable_list + # region + def test_incomparable_list(self): + actual = deep_equals.incomparable_list(4294967296) + + incomparable_class = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class.x = 0 + incomparable_class1 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class1.x = 1 + incomparable_class2 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class2.x = 2 + incomparable_class3 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class3.x = 3 + incomparable_class4 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class4.x = 4 + incomparable_class5 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class5.x = 5 + incomparable_class6 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class6.x = 6 + incomparable_class7 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class7.x = 7 + incomparable_class8 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class8.x = 8 + incomparable_class9 = copyreg._reconstructor(deep_equals.IncomparableClass, builtins.object, None) + incomparable_class9.x = 9 + expected_list = [incomparable_class, incomparable_class1, incomparable_class2, incomparable_class3, incomparable_class4, incomparable_class5, incomparable_class6, incomparable_class7, incomparable_class8, incomparable_class9] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + actual_x = actual_element.x + expected_x = expected_element.x + + self.assertEqual(expected_x, actual_x) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py b/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py new file mode 100644 index 0000000000..fa4f36dee3 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__deque.py @@ -0,0 +1,35 @@ +import sys +sys.path.append('samples') +import builtins +import deque +import collections +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable deque.generate_people_deque + # region + def test_generate_people_deque(self): + actual = deque.generate_people_deque(4294967297) + + deque1 = collections.deque() + deque1.append('Alex') + deque1.append('Bob') + deque1.append('Cate') + deque1.append('Daisy') + deque1.append('Ed') + + self.assertEqual(deque1, actual) + + def test_generate_people_deque1(self): + actual = deque.generate_people_deque(0) + + deque1 = collections.deque() + + self.assertEqual(deque1, actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py new file mode 100644 index 0000000000..61089ec196 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py @@ -0,0 +1,30 @@ +import sys +sys.path.append('samples') +import builtins +import dicts +import types +import unittest + + +class TestDictionary(unittest.TestCase): + # region Test suites for executable dicts.translate + # region + def test_translate(self): + dictionary = dicts.Dictionary([str(1.5 + 3.5j), str(1.5 + 3.5j), str('unicode remains unicode'), str(b'\x80'), str(-1234567890)], [{str(-123456789): str(1e+300 * 1e+300)}, {str(1.5 + 3.5j): str(), str(-123456789): str(), str(1e+300 * 1e+300): str(), str(): str(1e+300 * 1e+300)}]) + + actual = dictionary.translate(str(-1234567890), str(-123456789)) + + self.assertEqual(None, actual) + + def test_translate_throws_t(self): + dictionary = dicts.Dictionary([str(1e+300 * 1e+300), str(1.5 + 3.5j), str(1e+300 * 1e+300), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(1.5 + 3.5j), str(-1234567890)], [{str(b'\x80'): str(1e+300 * 1e+300), str(-123456789): str(1e+300 * 1e+300)}, {str(b'\x80'): str(1e+300 * 1e+300), str(b'\xf0\xa3\x91\x96', 'utf-8'): str(), str(id): str(1e+300 * 1e+300), str(): str()}, {str(b'\x80'): str(1e+300 * 1e+300), str(-123456789): str(), str(): str()}, {str(1e+300 * 1e+300): str(), str(-123456789): str(1e+300 * 1e+300)}, {str(1.5 + 3.5j): str(), str(1e+300 * 1e+300): str()}]) + + dictionary.translate(str(), str('unicode remains unicode')) + + # raises builtins.KeyError + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py new file mode 100644 index 0000000000..f6eed2d5ab --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_with_eq.py @@ -0,0 +1,39 @@ +import sys +sys.path.append('samples') +import dummy_with_eq +import builtins +import copyreg +import types +import unittest + + +class TestDummy(unittest.TestCase): + # region Test suites for executable dummy_with_eq.propagate + # region + def test_propagate(self): + dummy = dummy_with_eq.Dummy(1) + + actual = dummy.propagate() + + dummy1 = copyreg._reconstructor(dummy_with_eq.Dummy, builtins.object, None) + dummy1.field = 1 + expected_list = [dummy1, dummy1] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + actual_field = actual_element.field + expected_field = expected_element.field + + self.assertEqual(expected_field, actual_field) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py new file mode 100644 index 0000000000..3a38c1f6b7 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dummy_without_eq.py @@ -0,0 +1,36 @@ +import sys +sys.path.append('samples') +import dummy_without_eq +import builtins +import copyreg +import types +import unittest + + +class TestDummy(unittest.TestCase): + # region Test suites for executable dummy_without_eq.propagate + # region + def test_propagate(self): + dummy = dummy_without_eq.Dummy() + + actual = dummy.propagate() + + dummy1 = copyreg._reconstructor(dummy_without_eq.Dummy, builtins.object, None) + expected_list = [dummy1, dummy1] + expected_length = len(expected_list) + actual_length = len(actual) + + self.assertEqual(expected_length, actual_length) + + index = None + for index in range(0, expected_length, 1): + expected_element = expected_list[index] + actual_element = actual[index] + + self.assertTrue(isinstance(actual_element, dummy_without_eq.Dummy)) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py b/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py new file mode 100644 index 0000000000..c383980fb6 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__graph.py @@ -0,0 +1,36 @@ +import sys +sys.path.append('samples') +import unittest +import builtins +import graph +import copyreg +import types + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable graph.bfs + # region + def test_bfs(self): + actual = graph.bfs([graph.Node(str(1e+300 * 1e+300), []), graph.Node(str(id), []), graph.Node(str('unicode remains unicode'), [])]) + + node = copyreg._reconstructor(graph.Node, builtins.object, None) + node.name = 'unicode remains unicode' + node.children = [] + node1 = copyreg._reconstructor(graph.Node, builtins.object, None) + node1.name = '' + node1.children = [] + node2 = copyreg._reconstructor(graph.Node, builtins.object, None) + node2.name = 'inf' + node2.children = [] + self.assertEqual([node, node1, node2], actual) + + def test_bfs1(self): + actual = graph.bfs([]) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py b/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py new file mode 100644 index 0000000000..4339e26c0e --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__list_of_datetime.py @@ -0,0 +1,32 @@ +import sys +sys.path.append('samples') +import builtins +import list_of_datetime +import types +import datetime +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable list_of_datetime.get_data_labels + # region + def test_get_data_labels(self): + actual = list_of_datetime.get_data_labels({}) + + self.assertEqual(None, actual) + + def test_get_data_labels1(self): + actual = list_of_datetime.get_data_labels([datetime.time(0), datetime.time(microsecond=40), datetime.time(18, 45, 3, 1234), datetime.time(12, 0)]) + + self.assertEqual(['00:00', '00:00', '18:45', '12:00'], actual) + + def test_get_data_labels2(self): + actual = list_of_datetime.get_data_labels([datetime.time(microsecond=40), datetime.time()]) + + self.assertEqual(['1900-01-01', '1900-01-01'], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py b/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py new file mode 100644 index 0000000000..38d5289f30 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__lists.py @@ -0,0 +1,21 @@ +import sys +sys.path.append('samples') +import builtins +import lists +import datetime +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable lists.find_articles_with_author + # region + def test_find_articles_with_author(self): + actual = lists.find_articles_with_author([lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str('unicode remains unicode'), datetime.datetime(2015, 4, 5, 1, 45)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str('unicode remains unicode'), datetime.datetime(2011, 1, 1)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(), datetime.datetime(1, 2, 3, 4, 5, 6, 7)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(1, 2, 3, 4, 5, 6, 7)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(2014, 11, 2, 1, 30)), lists.Article(str(-123456789), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), datetime.datetime(1, 2, 3, 4, 5, 6, 7))], str('unicode remains unicode')) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py b/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py new file mode 100644 index 0000000000..510171cbbc --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__longest_subsequence.py @@ -0,0 +1,25 @@ +import sys +sys.path.append('samples') +import builtins +import longest_subsequence +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable longest_subsequence.longest_subsequence + # region + def test_longest_subsequence(self): + actual = longest_subsequence.longest_subsequence([1, 83]) + + self.assertEqual([1, 83], actual) + + def test_longest_subsequence1(self): + actual = longest_subsequence.longest_subsequence([2, -1, 4294967296]) + + self.assertEqual([-1, 4294967296], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py b/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py new file mode 100644 index 0000000000..db4d57758e --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__matrix.py @@ -0,0 +1,41 @@ +import sys +sys.path.append('samples') +import matrix +import builtins +import types +import copyreg +import unittest + + +class TestMatrix(unittest.TestCase): + # region Test suites for executable matrix.__add__ + + # region + + def test__add__(self): + matrix1 = matrix.Matrix([[float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(10 ** 23), float('1.4'), float(-1), float(-1), float('nan'), float('nan'), float(1970)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(314), float(-1), float('nan'), float(1970), 7.3, float(-1), float(-1)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float('nan')]]) + self1 = matrix.Matrix([[float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(10 ** 23), float('1.4'), float(-1), float(-1), float('nan'), float('nan'), float(1970)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float(314), float(-1), float('nan'), float(1970), 7.3, float(-1), float(-1)], [float('nan'), 0.0, float(1970), 7.3, float('nan')], [float('nan')]]) + + actual = matrix1.__add__(self1) + + matrix2 = copyreg._reconstructor(matrix.Matrix, builtins.object, None) + matrix2.dim = (6, 7) + matrix2.elements = [[float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [2e+23, 2.8, -2.0, -2.0, float('nan'), float('nan'), 3940.0], [float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [628.0, -2.0, float('nan'), 3940.0, 14.6, -2.0, -2.0], [float('nan'), 0.0, 3940.0, 14.6, float('nan'), 0, 0], [float('nan'), 0, 0, 0, 0, 0, 0]] + actual_dim = actual.dim + expected_dim = matrix2.dim + + self.assertEqual(expected_dim, actual_dim) + actual_elements = actual.elements + expected_elements = matrix2.elements + expected_list = expected_elements + expected_length = len(expected_list) + actual_length = len(actual_elements) + + self.assertEqual(expected_length, actual_length) + + self.assertTrue(isinstance(actual_elements, builtins.list)) + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py b/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py new file mode 100644 index 0000000000..16f8f8423f --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__primitive_types.py @@ -0,0 +1,40 @@ +import sys +sys.path.append('samples') +import builtins +import primitive_types +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable primitive_types.pretty_print + # region + def test_pretty_print(self): + actual = primitive_types.pretty_print(object()) + + self.assertEqual('I do not have any variants', actual) + + def test_pretty_print1(self): + actual = primitive_types.pretty_print(str(b'\x80')) + + self.assertEqual("It is string.\nValue <>", actual) + + def test_pretty_print2(self): + actual = primitive_types.pretty_print((1 << 100)) + + self.assertEqual('It is integer.\nValue 1267650600228229401496703205376', actual) + + def test_pretty_print3(self): + actual = primitive_types.pretty_print(complex(float('inf'), float('inf'))) + + self.assertEqual('It is complex.\nValue (inf + infi)', actual) + + def test_pretty_print4(self): + actual = primitive_types.pretty_print([]) + + self.assertEqual('It is list.\nValue []', actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py b/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py new file mode 100644 index 0000000000..ea0cf1a849 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__quick_sort.py @@ -0,0 +1,30 @@ +import sys +sys.path.append('samples') +import builtins +import quick_sort +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable quick_sort.quick_sort + # region + def test_quick_sort(self): + actual = quick_sort.quick_sort([4294967297, 83, (1 << 100), 4294967297, (1 << 100), 0, -3]) + + self.assertEqual([-3, 0, 83, 4294967297, 4294967297, 1267650600228229401496703205376, 1267650600228229401496703205376], actual) + + def test_quick_sort1(self): + actual = quick_sort.quick_sort([83, 123]) + + self.assertEqual([83, 123], actual) + + def test_quick_sort2(self): + actual = quick_sort.quick_sort([]) + + self.assertEqual([], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py b/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py new file mode 100644 index 0000000000..02bcda08b6 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__test_coverage.py @@ -0,0 +1,35 @@ +import sys +sys.path.append('samples') +import builtins +import test_coverage +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable test_coverage.hard_function + # region + def test_hard_function(self): + actual = test_coverage.hard_function(83) + + self.assertEqual(2, actual) + + def test_hard_function1(self): + actual = test_coverage.hard_function(0) + + self.assertEqual(1, actual) + + def test_hard_function2(self): + actual = test_coverage.hard_function(4294967296) + + self.assertEqual(3, actual) + + def test_hard_function3(self): + actual = test_coverage.hard_function(float('nan')) + + self.assertEqual(4, actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py b/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py new file mode 100644 index 0000000000..450f3f0488 --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__type_inference.py @@ -0,0 +1,20 @@ +import sys +sys.path.append('samples') +import builtins +import type_inference +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable type_inference.type_inference + # region + def test_type_inference_by_fuzzer(self): + actual = type_inference.type_inference(0, str(), str(b'\x80'), [], {}) + + self.assertEqual([0, 0, 0, 0, 0, 0, 0, 0, 0, ''], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py b/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py new file mode 100644 index 0000000000..f9fe1c7d4a --- /dev/null +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__using_collections.py @@ -0,0 +1,23 @@ +import sys +sys.path.append('samples') +import builtins +import using_collections +import collections +import unittest + + +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable using_collections.generate_collections + # region + def test_generate_collections(self): + actual = using_collections.generate_collections({}) + + counter = collections.Counter({0: 100, }) + + self.assertEqual([{0: 100, }, counter, [(0, 100)]], actual) + + # endregion + + # endregion + + diff --git a/utbot-python/samples/easy_samples/.gitignore b/utbot-python/samples/easy_samples/.gitignore new file mode 100644 index 0000000000..aef5dc69ef --- /dev/null +++ b/utbot-python/samples/easy_samples/.gitignore @@ -0,0 +1,3 @@ +utbot_tests +.tmp +.pytest_cache \ No newline at end of file diff --git a/utbot-python/samples/easy_samples/corner_cases.py b/utbot-python/samples/easy_samples/corner_cases.py new file mode 100644 index 0000000000..2805e84108 --- /dev/null +++ b/utbot-python/samples/easy_samples/corner_cases.py @@ -0,0 +1,16 @@ +from dataclasses import dataclass +import sample_classes as s + + +@dataclass +class Inner: + x: int + + +class A: + def __init__(self, x: Inner): + self.x = x + + def f(cls, self, x: Inner): + self.x += 1 + return cls.x.x, self, x diff --git a/utbot-python/samples/easy_samples/deep_equals.py b/utbot-python/samples/easy_samples/deep_equals.py new file mode 100644 index 0000000000..e6acdb7b6c --- /dev/null +++ b/utbot-python/samples/easy_samples/deep_equals.py @@ -0,0 +1,74 @@ + + +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class BadClass: + def __init__(self, x): + self.x = x + + +def return_bad_class(x: int): + return BadClass(x) + + +def return_comparable_class(x: int): + return ComparableClass(x) + + +def primitive_list(x: int): + return [x] * 10 + + +def primitive_set(x: int): + return set(x+i for i in range(5)) + + +def primitive_dict(x: str, y: int): + return {x: y} + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def bad_list(length: int): + return [BadClass(x) for x in range(min(length, 10))] + + +class Node: + def __init__(self, name: str): + self.name = name + self.children = [] + + def __str__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def cycle(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + a.children.append(b) + b.children.append(a) + return a + + +def cycle2(x: str): + a = Node(x + '_a') + b = Node(x + '_b') + c = Node(x + '_c') + a.children.append(b) + b.children.append(c) + c.children.append(a) + return a diff --git a/utbot-python/samples/easy_samples/empty_file.py b/utbot-python/samples/easy_samples/empty_file.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-python/samples/easy_samples/fully_annotated.py b/utbot-python/samples/easy_samples/fully_annotated.py new file mode 100644 index 0000000000..15854099d1 --- /dev/null +++ b/utbot-python/samples/easy_samples/fully_annotated.py @@ -0,0 +1,78 @@ +import heapq +from datetime import datetime +from typing import Any, List, Union, NoReturn + +""" +Default functions suite: fully annotated. +""" + + +def id_(x: Any) -> Any: + return x + + +def compare_with_5(x: int) -> bool: + return x > 5 + + +def add(x: int, y: int) -> int: + return x + y + + +def add_with_unused_param(x: int, y: int, unused: int) -> int: + return x + y + + +def append_exclamation_mark(s: str) -> str: + return s + "!" + + +def append_two_ints_to_typing_list(l: List[int]) -> List[int]: + return l + [1, -1] + + +def append_two_ints_to_builtin_list(l: list) -> list: + return l + [1, -1] + + +def append_ints_and_chars(l: List[Union[int, str]]) -> List[Union[int, str]]: + return l + [1, -1] + list("ab") + + +def format_data_labels(dates: List[datetime]) -> List[str]: + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + + +class ClassWithIntField: + def __init__(self, int_field_value): + self.int_field = int_field_value + + +class ClassWithAnnotatedIntField: + def __init__(self, int_field: int): + self.int_field = int_field + + +def inc_int_field(c: ClassWithIntField) -> int: + c.int_field += 1 + return c.int_field + + +def call_heapify(ints: List[int]) -> List[int]: + heapq.heapify(ints) + return ints + + +def concatenate_args(*args: str) -> str: + return "+".join(args) + + +def concatenate_args_and_kwargs(*args: str, **kwargs: str) -> str: + return "+".join(args) + ";" + "?".join(kwargs.values()) + + +def raise_exception(exc: Exception) -> NoReturn: + raise exc diff --git a/utbot-python/samples/easy_samples/general.py b/utbot-python/samples/easy_samples/general.py new file mode 100644 index 0000000000..16c492eeb8 --- /dev/null +++ b/utbot-python/samples/easy_samples/general.py @@ -0,0 +1,193 @@ +import collections +import heapq +import typing +from socket import socket +from typing import * +from dataclasses import dataclass +import logging +import datetime + + +class Dummy: + def propagate(self): + return [self, self] + + + +class A: + x = 4 + y = 5 + + def func(self): + n = 0 + for i in range(self.x): + n += self.y + return n + + +def fact(n): + ans = 1 + for i in range(1, n + 1): + ans *= i + return ans + + +def empty_(): + pass + + +def conditions(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x.real + elif len(str(x)) > 3: + return 3 + else: + return 4 + + +def test_call(x): + return repr_test(x) + + +def zero_division(x): + return x / x + + +def repr_test(x): + x *= 100 + return [1, x + 1, collections.UserList([1, 2, 3]), collections.Counter("flkafksdf"), collections.OrderedDict({1: 2, 4: "jflas"})] + + +def str_test(x): + x += '1"23' + x += "flskjd'jfslk" + if len(x.split('.')) == 1: + return '1"23' + else: + return """100''500""" + + +def return_socket(x: int): + return socket() + + +def empty(): + return 1 + + +def id_(x): + return x + + +def f(x, y, z, a, c, d, e, g, h, i): + if y % 2 == 0: + x = 1 + y + z += "aba" + a += [2] + list("str") + i + A = c < "abc" + B = "abc" == d + e = {1, 2, 3} + C = g == {1: 2} + h += int("777") + return x + y + + +def g(x: List[int], y: List): + y[0] += 1 + return x, y + + +def i(x: Dict[int, int]): + return x[0] + + +def j(x: Set[int]): + return x + + +def h(x): + if x < 123: + return 1 + return 2 + + +def a(x): + x.description += 1 + return x.description + + +def sqrt(x): + return x.sqrt() + + +@dataclass +class InventoryItem: + name: str + + +def inv(x): + return x.name + "aba" # interesting case with io.BytesIO + + +def b(x, y): + y = len(x) + return bytes(x, 'utf-8') + + +def c(x): + return heapq.heapify(x) + + +def d(x: Optional[int]): + return x + + +def k(x: typing.Any): + if x == complex(1): + return x + + +def constants(x): + if x == 1e5: + return "one" + elif (x > 1e4 - 2) and (x < 1e4): + return "two" + else: + return "three" + + +# interesting case with sets +def get_data_labels(dates): + if len(dates) == 0: + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] + + +# bad function +def m(x): + x = frozenset() + return len(x + 1) + + +# very bad function +def n(x, y): + y = (-x) + 1 + x *= 10 + # z = -x + print(x) + x = len([1]) + if y == len([1]): + y += print() + return x.description + + +def list_of_list(x: List[List[InventoryItem]]): + return x diff --git a/utbot-python/samples/easy_samples/sample_classes.py b/utbot-python/samples/easy_samples/sample_classes.py new file mode 100644 index 0000000000..772d5b0740 --- /dev/null +++ b/utbot-python/samples/easy_samples/sample_classes.py @@ -0,0 +1,21 @@ +from dataclasses import dataclass + +class A: + def __init__(self, val: int): + self.description = val + + +class B: + def __init__(self, val: complex): + self.description = val + + def sqrt(self): + return self.description ** 0.5 + + +@dataclass +class C: + counter: int = 0 + + def inc(self): + self.counter += 1 diff --git a/utbot-python/samples/generate_test_samples.sh b/utbot-python/samples/generate_test_samples.sh new file mode 100644 index 0000000000..73c765a72b --- /dev/null +++ b/utbot-python/samples/generate_test_samples.sh @@ -0,0 +1,24 @@ +# Usage: +# ./generate_test_samples.sh + +python_path=$1 +java_path=$2 + +$java_path -jar utbot-cli.jar generate_python samples/arithmetic.py -p $python_path -o cli_utbot_tests/generated_tests__arithmetic.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/deep_equals.py -p $python_path -o cli_utbot_tests/generated_tests__deep_equals.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dicts.py -p $python_path -o cli_utbot_tests/generated_tests__dicts.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dictionary -m translate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/deque.py -p $python_path -o cli_utbot_tests/generated_tests__deque.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_with_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_with_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dummy -m propogate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_without_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_without_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Dummy -m propogate --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/lists.py -p $python_path -o cli_utbot_tests/generated_tests__lists.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/list_of_datetime.py -p $python_path -o cli_utbot_tests/generated_tests__list_of_datetime.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/longest_subsequence.py -p $python_path -o cli_utbot_tests/generated_tests__longest_subsequence.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/matrix.py -p $python_path -o cli_utbot_tests/generated_tests__matrix.py -s samples/ --timeout-for-run 500 --visit-only-specified-source -c Matrix -m __add__,__mul__,__matmul__ --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/primitive_types.py -p $python_path -o cli_utbot_tests/generated_tests__primitive_types.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/quick_sort.py -p $python_path -o cli_utbot_tests/generated_tests__quick_sort.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/test_coverage.py -p $python_path -o cli_utbot_tests/generated_tests__test_coverage.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/type_inference.py -p $python_path -o cli_utbot_tests/generated_tests__type_inference.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/using_collections.py -p $python_path -o cli_utbot_tests/generated_tests__using_collections.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_without_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_without_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 -c Dummy -m propagate --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/dummy_with_eq.py -p $python_path -o cli_utbot_tests/generated_tests__dummy_with_eq.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 -c Dummy -m propagate --do-not-check-requirements +$java_path -jar utbot-cli.jar generate_python samples/list_of_datetime.py -p $python_path -o cli_utbot_tests/generated_tests__list_of_datetime.py -s samples/ --timeout-for-run 500 --visit-only-specified-source --timeout 10000 --do-not-check-requirements diff --git a/utbot-python/samples/run_test_samples.sh b/utbot-python/samples/run_test_samples.sh new file mode 100755 index 0000000000..bbbf1bb171 --- /dev/null +++ b/utbot-python/samples/run_test_samples.sh @@ -0,0 +1,24 @@ +# Usage: +# ./run_test_samples.sh + +python_path=$1 +java_path=$2 + +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__arithmetic.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__deep_equals.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dicts.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__deque.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_with_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_without_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__lists.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__list_of_datetime.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__longest_subsequence.py -p $python_path +$java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__matrix.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__primitive_types.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__quick_sort.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__test_coverage.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__type_inference.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__using_collections.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_with_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__dummy_without_eq.py -p $python_path +# $java_path -jar utbot-cli.jar --verbosity DEBUG run_python cli_utbot_tests/generated_tests__list_of_datetime.py -p $python_path diff --git a/utbot-python/samples/samples.md b/utbot-python/samples/samples.md new file mode 100644 index 0000000000..b3cf2794e8 --- /dev/null +++ b/utbot-python/samples/samples.md @@ -0,0 +1,27 @@ +## Соответствие файлов и сгенерированных тестов + +Примеры в `/samples`, сгенерированный код в `/cli_utbot_tests`. + +Команда по умолчанию +```bash +java -jar utbot-cli.jar generate_python samples/.py -p -o cli_utbot_tests/.py -s samples/ ----timeout-for-run 500 --timeout 10000 --visit-only-specified-source +``` + +| Пример | Тесты | Дополнительные аргументы | +|--------------------------|-------------------------------------------|-------------------------------------------| +| `arithmetic.py` | `generated_tests__arithmetic.py` | | +| `deep_equals.py` | `generated_tests__deep_equals.py` | | +| `dicts.py` | `generated_tests__dicts.py` | `-c Dictionary -m translate` | +| `deque.py` | `generated_tests__deque.py` | | +| `dummy_with_eq.py` | `generated_tests__dummy_with_eq.py` | `-c Dummy -m propogate` | +| `dummy_without_eq.py` | `generated_tests__dummy_without_eq.py` | `-c Dummy -m propogate` | +| `graph.py` | `generated_tests__graph.py` | | +| `lists.py` | `generated_tests__lists.py` | | +| `list_of_datetime.py` | `generated_tests__list_of_datetime.py` | | +| `longest_subsequence.py` | `generated_tests__longest_subsequence.py` | | +| `matrix.py` | `generated_tests__matrix.py` | `-c Matrix -m __add__,__mul__,__matmul__` | +| `primitive_types.py` | `generated_tests__primitive_types.py` | | +| `quick_sort.py` | `generated_tests__quick_sort.py` | | +| `test_coverage.py` | `generated_tests__test_coverage.py` | | +| `type_inhibition.py` | `generated_tests__type_inhibition.py` | | +| `using_collections.py` | `generated_tests__using_collections.py` | | diff --git a/utbot-python/samples/samples/arithmetic.py b/utbot-python/samples/samples/arithmetic.py new file mode 100644 index 0000000000..6f26aa94f4 --- /dev/null +++ b/utbot-python/samples/samples/arithmetic.py @@ -0,0 +1,17 @@ +import math + + +def calculate_function_value(x, y): + """ + Calculate value `f` + | sqrt(x - 2y) , x > 100 + f(x, y) = | (3x^2 - 2xy + y^2) / sin(x) , -100 < x <= 100 + | (0.01 * x) ^ log2(y) , x < -100 + """ + + if x > 100: + return math.sqrt(x - 2 * y) + elif -100 < x <= 100: + return (3*x**2 - 2*x*y + y**2) / math.sin(x) + else: + return (0.01 * x) ** math.log2(y) diff --git a/utbot-python/samples/samples/deep_equals.py b/utbot-python/samples/samples/deep_equals.py new file mode 100644 index 0000000000..f34cb42c5b --- /dev/null +++ b/utbot-python/samples/samples/deep_equals.py @@ -0,0 +1,22 @@ +class ComparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return self.x == other.x + + +class IncomparableClass: + def __init__(self, x): + self.x = x + + def __eq__(self, other): + return id(self) == id(other) + + +def comparable_list(length: int): + return [ComparableClass(x) for x in range(min(length, 10))] + + +def incomparable_list(length: int): + return [IncomparableClass(x) for x in range(min(length, 10))] diff --git a/utbot-python/samples/samples/deque.py b/utbot-python/samples/samples/deque.py new file mode 100644 index 0000000000..87ea0e958e --- /dev/null +++ b/utbot-python/samples/samples/deque.py @@ -0,0 +1,8 @@ +from collections import deque + + +def generate_people_deque(people_count: int): + names = ['Alex', 'Bob', 'Cate', 'Daisy', 'Ed'] + if people_count > 5: + people_count = 5 + return deque(sorted(names[:people_count])) \ No newline at end of file diff --git a/utbot-python/samples/samples/dicts.py b/utbot-python/samples/samples/dicts.py new file mode 100644 index 0000000000..6c63404587 --- /dev/null +++ b/utbot-python/samples/samples/dicts.py @@ -0,0 +1,29 @@ +from typing import List, Dict, Optional + + +class Word: + def __init__(self, translations: Dict[str, str]): + self.translations = translations + + def keys(self): + return list(self.translations.keys()) + + +class Dictionary: + def __init__( + self, + languages: List[str], + words: List[Dict[str, str]], + ): + self.languages = languages + self.words = [Word(translations) for translations in words] + + def translate(self, word: str, language: Optional[str]): + if language is not None: + for word_ in self.words: + if word_.translations[language] == word: + return word_ + else: + for word_ in self.words: + if word in word_.translations.values(): + return word_ diff --git a/utbot-python/samples/samples/dummy_with_eq.py b/utbot-python/samples/samples/dummy_with_eq.py new file mode 100644 index 0000000000..05ef7b822e --- /dev/null +++ b/utbot-python/samples/samples/dummy_with_eq.py @@ -0,0 +1,9 @@ +class Dummy: + def __init__(self, value: int): + self.field = value + + def __eq__(self, other): + return self.field == other.field + + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/dummy_without_eq.py b/utbot-python/samples/samples/dummy_without_eq.py new file mode 100644 index 0000000000..8df59b68d2 --- /dev/null +++ b/utbot-python/samples/samples/dummy_without_eq.py @@ -0,0 +1,3 @@ +class Dummy: + def propagate(self): + return [self, self] diff --git a/utbot-python/samples/samples/graph.py b/utbot-python/samples/samples/graph.py new file mode 100644 index 0000000000..727bcb7ba2 --- /dev/null +++ b/utbot-python/samples/samples/graph.py @@ -0,0 +1,42 @@ +from __future__ import annotations +from collections import deque +from typing import List + + +class Node: + def __init__(self, name: str, children: List[Node]): + self.name = name + self.children = children + + def __repr__(self): + return f'' + + def __eq__(self, other): + if isinstance(other, Node): + return self.name == other.name + else: + return False + + +def bfs(nodes: List[Node]): + if len(nodes) == 0: + return [] + + visited = [] + queue = deque(nodes) + while len(queue) > 0: + node = queue.pop() + if node not in visited: + visited.append(node) + for child in node.children: + queue.append(child) + return visited + + +if __name__ == '__main__': + a = Node('a', []) + b = Node('b', []) + c = Node('c', []) + a.children.append(b) + b.children.append(c) + print(bfs([a, b, c])) diff --git a/utbot-python/samples/samples/list_of_datetime.py b/utbot-python/samples/samples/list_of_datetime.py new file mode 100644 index 0000000000..d23d306710 --- /dev/null +++ b/utbot-python/samples/samples/list_of_datetime.py @@ -0,0 +1,10 @@ +import datetime + + +def get_data_labels(dates): + if len(dates) == 0: + return None + if all(x.hour == 0 and x.minute == 0 for x in dates): + return [x.strftime('%Y-%m-%d') for x in dates] + else: + return [x.strftime('%H:%M') for x in dates] diff --git a/utbot-python/samples/samples/lists.py b/utbot-python/samples/samples/lists.py new file mode 100644 index 0000000000..daa1472ac5 --- /dev/null +++ b/utbot-python/samples/samples/lists.py @@ -0,0 +1,25 @@ +from dataclasses import dataclass +import datetime +from typing import List + + +@dataclass +class Article: + title: str + author: str + content: str + created_at: datetime.datetime + + +def find_articles_with_author(articles: List[Article], author: str) -> List[Article]: + return [ + article for article in articles + if article.author == author + ] + + +if __name__ == '__main__': + print(find_articles_with_author([ + Article('a', 'a1', 'jfls', datetime.datetime.today()), + Article('b', 'a2', 'fjls', datetime.datetime.now()) + ], 'a1')) diff --git a/utbot-python/samples/samples/longest_subsequence.py b/utbot-python/samples/samples/longest_subsequence.py new file mode 100644 index 0000000000..1200f8fb5a --- /dev/null +++ b/utbot-python/samples/samples/longest_subsequence.py @@ -0,0 +1,27 @@ +from typing import List + + +def longest_subsequence(array: List[int]) -> List[int]: + array_length = len(array) + if array_length <= 1: + return array + pivot = array[0] + is_found = False + i = 1 + longest_subseq: List[int] = [] + while not is_found and i < array_length: + if array[i] < pivot: + is_found = True + temp_array = [element for element in array[i:] if element >= array[i]] + temp_array = longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + longest_subseq = temp_array + else: + i += 1 + + temp_array = [element for element in array[1:] if element >= pivot] + temp_array = [pivot] + longest_subsequence(temp_array) + if len(temp_array) > len(longest_subseq): + return temp_array + else: + return longest_subseq diff --git a/utbot-python/samples/samples/matrix.py b/utbot-python/samples/samples/matrix.py new file mode 100644 index 0000000000..1e3e58ab7f --- /dev/null +++ b/utbot-python/samples/samples/matrix.py @@ -0,0 +1,69 @@ +from __future__ import annotations +from itertools import product +from typing import List + + +class MatrixException(Exception): + def __init__(self, description): + self.description = description + + +class Matrix: + def __init__(self, elements: List[List[float]]): + self.dim = ( + len(elements), + max(len(elements[i]) for i in range(len(elements))) + if len(elements) > 0 else 0 + ) + self.elements = [ + row + [0] * (self.dim[1] - len(row)) + for row in elements + ] + + def __repr__(self): + return str(self.elements) + + def __eq__(self, other): + if isinstance(other, Matrix): + return self.elements == other.elements + + def __add__(self, other: Matrix): + if self.dim == other.dim: + return Matrix([ + [ + elem + other_elem for elem, other_elem in + zip(self.elements[i], other.elements[i]) + ] + for i in range(self.dim[0]) + ]) + + def __mul__(self, other): + if isinstance(other, (int, float, complex)): + return Matrix([ + [ + elem * other for elem in + self.elements[i] + ] + for i in range(self.dim[0]) + ]) + else: + raise MatrixException("Wrong Type") + + def __matmul__(self, other): + if isinstance(other, Matrix): + if self.dim[1] == other.dim[0]: + result = [[0 for _ in range(self.dim[0])] * other.dim[1]] + for i, j in product(range(self.dim[0]), range(other.dim[1])): + result[i][j] = sum( + self.elements[i][k] * other.elements[k][j] + for k in range(self.dim[1]) + ) + return Matrix(result) + else: + raise MatrixException("Wrong Type") + + +if __name__ == '__main__': + a = Matrix([[1., 2.]]) + b = Matrix([[3.], [4.]]) + print(a @ b) diff --git a/utbot-python/samples/samples/primitive_types.py b/utbot-python/samples/samples/primitive_types.py new file mode 100644 index 0000000000..0f95078654 --- /dev/null +++ b/utbot-python/samples/samples/primitive_types.py @@ -0,0 +1,11 @@ +def pretty_print(x): + if isinstance(x, int): + return 'It is integer.\n' + 'Value ' + str(x) + elif isinstance(x, str): + return 'It is string.\n' + 'Value <<' + x + '>>' + elif isinstance(x, complex): + return 'It is complex.\n' + 'Value (' + str(x.real) + ' + ' + str(x.real) + 'i)' + elif isinstance(x, list): + return 'It is list.\n' + f'Value {x}' + else: + return 'I do not have any variants' diff --git a/utbot-python/samples/samples/quick_sort.py b/utbot-python/samples/samples/quick_sort.py new file mode 100644 index 0000000000..a58f6d3bea --- /dev/null +++ b/utbot-python/samples/samples/quick_sort.py @@ -0,0 +1,32 @@ +import random +from typing import List + + +def quick_sort(array: List[int]): + def partition(A, left_index, right_index): + pivot = A[left_index] + i = left_index + 1 + for j in range(left_index + 1, right_index): + if A[j] < pivot: + A[j], A[i] = A[i], A[j] + i += 1 + A[left_index], A[i - 1] = A[i - 1], A[left_index] + return i - 1 + + def quick_sort_random(A, left, right): + if left < right: + pivot = random.randint(left, right - 1) + A[pivot], A[left] = ( + A[left], + A[pivot], + ) # switches the pivot with the left most bound + pivot_index = partition(A, left, right) + quick_sort_random( + A, left, pivot_index + ) # recursive quicksort to the left of the pivot point + quick_sort_random( + A, pivot_index + 1, right + ) # recursive quicksort to the right of the pivot point + quick_sort_random(array, 0, len(array)) + return array + diff --git a/utbot-python/samples/samples/test_coverage.py b/utbot-python/samples/samples/test_coverage.py new file mode 100644 index 0000000000..9a2740abfb --- /dev/null +++ b/utbot-python/samples/samples/test_coverage.py @@ -0,0 +1,12 @@ +def hard_function(x): + if x % 100 == 0: + return 1 + elif x + 100 < 400: + return 2 + else: + if x == complex(1, 2): + return x + elif len(str(x)) > 3: + return 3 + else: + return 4 diff --git a/utbot-python/samples/samples/type_inference.py b/utbot-python/samples/samples/type_inference.py new file mode 100644 index 0000000000..5cc524df1e --- /dev/null +++ b/utbot-python/samples/samples/type_inference.py @@ -0,0 +1,16 @@ +def type_inference(number, string, string_sep, list_of_number, dict_str_to_list): + new_string = '_' + string + '_' * number + new_string = new_string.capitalize() + string_sep + new_string[::-1] + + if len(list_of_number) < len(new_string): + list_of_number += [0] * (len(new_string) - len(list_of_number)) + + dict_str_to_list[string] = [] + for key in dict_str_to_list.keys(): + list_of_number.append(key) + + return list_of_number + + +if __name__ == '__main__': + print(type_inference(5, 'fjsl', '|', [1, 2, 3], {'fjls': [1, 2]})) \ No newline at end of file diff --git a/utbot-python/samples/samples/using_collections.py b/utbot-python/samples/samples/using_collections.py new file mode 100644 index 0000000000..debdc31bef --- /dev/null +++ b/utbot-python/samples/samples/using_collections.py @@ -0,0 +1,15 @@ +import collections + + +def generate_collections(collection): + collection[0] = 100 + elements = list(collection.items()) + return [ + collection, + collections.Counter(collection), + elements + ] + + +if __name__ == '__main__': + print(generate_collections({1: 2})) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt new file mode 100644 index 0000000000..0a0c5538b1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt @@ -0,0 +1,158 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonTreeModel +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.fuzz +import org.utbot.python.code.AnnotationProcessor.getModulesFromAnnotation +import org.utbot.python.providers.defaultPythonModelProvider +import org.utbot.python.utils.camelToSnakeCase +import org.utbot.summary.fuzzer.names.MethodBasedNameSuggester +import org.utbot.summary.fuzzer.names.ModelBasedNameSuggester +import org.utbot.fuzzer.FuzzedValue +import java.lang.Long.max + +private val logger = KotlinLogging.logger {} +const val CHUNK_SIZE = 15 + +class PythonEngine( + private val methodUnderTest: PythonMethod, + private val directoriesForSysPath: Set, + private val moduleToImport: String, + private val pythonPath: String, + private val fuzzedConcreteValues: List, + private val selectedTypeMap: Map, + private val timeoutForRun: Long, + private val initialCoveredLines: Set +) { + + private data class JobResult( + val evalResult: EvaluationResult, + val values: List, + val thisObject: UtModel?, + val modelList: List + ) + + fun fuzzing(): Sequence = sequence { + val types = methodUnderTest.arguments.map { + selectedTypeMap[it.name] ?: pythonAnyClassId + } + + val methodUnderTestDescription = FuzzedMethodDescription( + methodUnderTest.name, + pythonAnyClassId, + types, + fuzzedConcreteValues + ).apply { + compilableName = methodUnderTest.name // what's the difference with ordinary name? + parameterNameMap = { index -> methodUnderTest.arguments.getOrNull(index)?.name } + } + + val additionalModules = selectedTypeMap.values.flatMap { + getModulesFromAnnotation(it) + }.toSet() + + val evaluationInputIterator = fuzz(methodUnderTestDescription, defaultPythonModelProvider).map { values -> + val parameterValues = values.map { it.model } + val (thisObject, modelList) = + if (methodUnderTest.containingPythonClassId == null) + Pair(null, parameterValues) + else + Pair(parameterValues[0], parameterValues.drop(1)) + + EvaluationInput( + methodUnderTest, + parameterValues, + directoriesForSysPath, + moduleToImport, + pythonPath, + timeoutForRun, + thisObject, + modelList, + values, + additionalModules + ) + }.iterator() + + val coveredLines = initialCoveredLines.toMutableSet() + while (evaluationInputIterator.hasNext()) { + val chunk = mutableListOf() + while (evaluationInputIterator.hasNext() && chunk.size < CHUNK_SIZE) + chunk += evaluationInputIterator.next() + + val coveredBefore = coveredLines.size + val processes = chunk.map { evaluationInput -> + startEvaluationProcess(evaluationInput) + } + val startedTime = System.currentTimeMillis() + val results = (processes zip chunk).map { (process, evaluationInput) -> + val wait = max(10, timeoutForRun - (System.currentTimeMillis() - startedTime)) + val evalResult = getEvaluationResult(evaluationInput, process, wait) + JobResult( + evalResult, + evaluationInput.values, + evaluationInput.thisObject, + evaluationInput.modelList + ) + } + results.forEach { jobResult -> + if (jobResult.evalResult is EvaluationError) { + yield(UtError(jobResult.evalResult.reason, Throwable())) + } else { + val (resultJSON, isException, coverage) = jobResult.evalResult as EvaluationSuccess + + coverage.coveredInstructions.forEach { coveredLines.add(it.lineNumber) } + + val prohibitedExceptions = listOf( + "builtins.AttributeError", + "builtins.TypeError" + ) + if (isException && (resultJSON.type.name in prohibitedExceptions)) { // wrong type (sometimes mypy fails) + logger.debug("Evaluation with prohibited exception. Substituted types: ${ + types.joinToString { it.name } + }. Exception type: ${resultJSON.type.name}") + return@sequence + } + + val result = + if (isException) + UtExplicitlyThrownException(Throwable(resultJSON.output.type.toString()), false) // TODO: + else { + val outputType = resultJSON.type + val resultAsModel = PythonTreeModel( + resultJSON.output, + outputType + ) + UtExecutionSuccess(resultAsModel) + } + + val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester()) + val testMethodName = try { + nameSuggester.flatMap { it.suggest(methodUnderTestDescription, jobResult.values, result) }.firstOrNull() + } catch (t: Throwable) { + null + } + + yield( + UtExecution( + stateBefore = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), + stateAfter = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), + result = result, + coverage = coverage, + testMethodName = testMethodName?.testName?.camelToSnakeCase(), + displayName = testMethodName?.displayName, + ) + ) + } + } + val coveredAfter = coveredLines.size + + if (coveredAfter == coveredBefore) + return@sequence + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt new file mode 100644 index 0000000000..fc2a4dd591 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt @@ -0,0 +1,125 @@ +package org.utbot.python + +import com.beust.klaxon.Klaxon +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.PythonTree +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.fuzzer.FuzzedValue +import org.utbot.python.code.KlaxonPythonTreeParser +import org.utbot.python.code.PythonCodeGenerator +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.getResult +import org.utbot.python.utils.startProcess +import java.io.File + + +sealed class EvaluationResult +class EvaluationError(val reason: String) : EvaluationResult() +class EvaluationSuccess( + private val output: OutputData, + private val isException: Boolean, + val coverage: PythonCoverage +): EvaluationResult() { + operator fun component1() = output + operator fun component2() = isException + operator fun component3() = coverage +} + +data class OutputData(val output: PythonTree.PythonTreeNode, val type: PythonClassId) + +data class EvaluationInput( + val method: PythonMethod, + val methodArguments: List, + val directoriesForSysPath: Set, + val moduleToImport: String, + val pythonPath: String, + val timeoutForRun: Long, + val thisObject: UtModel?, + val modelList: List, + val values: List, + val additionalModulesToImport: Set = emptySet() +) + +data class EvaluationProcess ( + val process: Process, + val fileWithCode: File, + val fileForOutput: File +) + +fun startEvaluationProcess(input: EvaluationInput): EvaluationProcess { + val fileForOutput = TemporaryFileManager.assignTemporaryFile( + tag = "out_" + input.method.name + ".py", + addToCleaner = false + ) + val runCode = PythonCodeGenerator.generateRunFunctionCode( + input.method, + input.methodArguments, + input.directoriesForSysPath, + input.moduleToImport, + input.additionalModulesToImport, + fileForOutput.path + ) + val fileWithCode = TemporaryFileManager.createTemporaryFile( + runCode, + tag = "run_" + input.method.name + ".py", + addToCleaner = false + ) + return EvaluationProcess( + startProcess(listOf(input.pythonPath, fileWithCode.path)), + fileWithCode, + fileForOutput + ) +} + +fun getEvaluationResult(input: EvaluationInput, process: EvaluationProcess, timeout: Long): EvaluationResult { + val result = getResult(process.process, timeout = timeout) + process.fileWithCode.delete() + + if (result.exitValue != 0) + return EvaluationError( + if (result.terminatedByTimeout) "Timeout" else "Non-zero exit status" + ) + + val output = process.fileForOutput.readText().split('\n') + process.fileForOutput.delete() + + if (output.size != 4) + return EvaluationError("Incorrect format of output") + + val status = output[0] + + if (status != PythonCodeGenerator.successStatus && status != PythonCodeGenerator.failStatus) + return EvaluationError("Incorrect format of output") + + val isSuccess = status == PythonCodeGenerator.successStatus + + val pythonTree = KlaxonPythonTreeParser(output[1]).parseJsonToPythonTree() + val stmts = Klaxon().parseArray(output[2])!! + val missed = Klaxon().parseArray(output[3])!! + val covered = stmts.filter { it !in missed } + val coverage = PythonCoverage( + covered.map { + Instruction( + input.method.containingPythonClassId?.name ?: pythonAnyClassId.name, + input.method.methodSignature(), + it, + it.toLong() + ) + }, + missed.map { + Instruction( + input.method.containingPythonClassId?.name ?: pythonAnyClassId.name, + input.method.methodSignature(), + it, + it.toLong() + ) + } + ) + + return EvaluationSuccess( + OutputData(pythonTree, pythonTree.type), + !isSuccess, + coverage + ) +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt new file mode 100644 index 0000000000..4e505be17c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestCaseGenerator.kt @@ -0,0 +1,166 @@ +package org.utbot.python + +import mu.KotlinLogging +import org.utbot.framework.minimization.minimizeExecutions +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.python.code.ArgInfoCollector +import org.utbot.python.typing.AnnotationFinder.findAnnotations +import org.utbot.python.typing.MypyAnnotations +import org.utbot.python.utils.AnnotationNormalizer.annotationFromProjectToClassId +import java.nio.file.Path + +private val logger = KotlinLogging.logger {} + +object PythonTestCaseGenerator { + private var withMinimization: Boolean = true + private var pythonRunRoot: Path? = null + private lateinit var directoriesForSysPath: Set + private lateinit var curModule: String + private lateinit var pythonPath: String + private lateinit var fileOfMethod: String + private lateinit var isCancelled: () -> Boolean + private var timeoutForRun: Long = 0 + + fun init( + directoriesForSysPath: Set, + moduleToImport: String, + pythonPath: String, + fileOfMethod: String, + timeoutForRun: Long, + withMinimization: Boolean = true, + pythonRunRoot: Path? = null, + isCancelled: () -> Boolean + ) { + this.directoriesForSysPath = directoriesForSysPath + this.curModule = moduleToImport + this.pythonPath = pythonPath + this.fileOfMethod = fileOfMethod + this.withMinimization = withMinimization + this.isCancelled = isCancelled + this.timeoutForRun = timeoutForRun + this.pythonRunRoot = pythonRunRoot + } + + private val storageForMypyMessages: MutableList = mutableListOf() + + fun generate(method: PythonMethod): PythonTestSet { + storageForMypyMessages.clear() + + val initialArgumentTypes = method.arguments.map { + annotationFromProjectToClassId( + it.annotation, + pythonPath, + curModule, + fileOfMethod, + directoriesForSysPath + ) + }.toMutableList() + + // TODO: consider static and class methods + if (method.containingPythonClassId != null) { + initialArgumentTypes[0] = NormalizedPythonAnnotation(method.containingPythonClassId!!.name) + } + + logger.debug("Collecting hints about arguments") + val argInfoCollector = ArgInfoCollector(method, initialArgumentTypes) + logger.debug("Collected.") + val annotationSequence = getAnnotations(method, initialArgumentTypes, argInfoCollector, isCancelled) + + val executions = mutableListOf() + val errors = mutableListOf() + var missingLines: Set? = null + val coveredLines = mutableSetOf() + var generated = 0 + + run breaking@ { + annotationSequence.forEach { annotations -> + if (isCancelled()) + return@breaking + + logger.debug("Found annotations: ${ + annotations.map { "${it.key}: ${it.value}" }.joinToString(" ") + }") + + val engine = PythonEngine( + method, + directoriesForSysPath, + curModule, + pythonPath, + argInfoCollector.getConstants(), + annotations, + timeoutForRun, + coveredLines + ) + + engine.fuzzing().forEach { + if (isCancelled()) + return@breaking + generated += 1 + when (it) { + is UtExecution -> { + logger.debug("Added execution") + executions += it + missingLines = updateCoverage(it, coveredLines, missingLines) + } + is UtError -> { + logger.debug("Failed evaluation. Reason: ${it.description}") + errors += it + } + } + if (withMinimization && missingLines?.isEmpty() == true && generated % CHUNK_SIZE == 0) + return@breaking + } + } + } + + val (successfulExecutions, failedExecutions) = executions.partition { it.result is UtExecutionSuccess } + + return PythonTestSet( + method, + if (withMinimization) + minimizeExecutions(successfulExecutions) + minimizeExecutions(failedExecutions) + else + executions, + errors, + storageForMypyMessages + ) + } + + // returns new missingLines + private fun updateCoverage(execution: UtExecution, coveredLines: MutableSet, missingLines: Set?): Set { + execution.coverage?.coveredInstructions?.map { instr -> coveredLines.add(instr.lineNumber) } + val curMissing = + (execution.coverage as? PythonCoverage) + ?.missedInstructions + ?.map { x -> x.lineNumber } ?.toSet() + ?: emptySet() + return if (missingLines == null) curMissing else missingLines intersect curMissing + } + + private fun getAnnotations( + method: PythonMethod, + initialArgumentTypes: List, + argInfoCollector: ArgInfoCollector, + isCancelled: () -> Boolean + ): Sequence> { + + val existingAnnotations = mutableMapOf() + initialArgumentTypes.forEachIndexed { index, classId -> + if (classId != pythonAnyClassId) + existingAnnotations[method.arguments[index].name] = classId + } + + return findAnnotations( + argInfoCollector, + method, + existingAnnotations, + curModule, + directoriesForSysPath, + pythonPath, + isCancelled, + storageForMypyMessages + ) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt new file mode 100644 index 0000000000..96c79661cc --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonTestGenerationProcessor.kt @@ -0,0 +1,277 @@ +package org.utbot.python + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.utbot.framework.codegen.* +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.* +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.python.framework.codegen.model.PythonCodeGenerator +import org.utbot.python.typing.MypyAnnotations +import org.utbot.python.typing.PythonTypesStorage +import org.utbot.python.typing.StubFileFinder +import org.utbot.python.utils.Cleaner +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.RequirementsUtils.requirementsAreInstalled +import org.utbot.python.utils.getLineOfFunction +import java.io.File +import java.nio.file.Path +import kotlin.io.path.Path +import kotlin.io.path.pathString + +object PythonTestGenerationProcessor { + fun processTestGeneration( + pythonPath: String, + testSourceRoot: String, + pythonFilePath: String, + pythonFileContent: String, + directoriesForSysPath: Set, + currentPythonModule: String, + pythonMethods: List, + containingClassName: String?, + timeout: Long, + testFramework: TestFramework, + timeoutForRun: Long, + writeTestTextToFile: (String) -> Unit, + doNotCheckRequirements: Boolean = false, + visitOnlySpecifiedSource: Boolean = false, + withMinimization: Boolean = true, + isCanceled: () -> Boolean = { false }, + checkingRequirementsAction: () -> Unit = {}, + requirementsAreNotInstalledAction: () -> MissingRequirementsActionResult = { + MissingRequirementsActionResult.NOT_INSTALLED + }, + startedLoadingPythonTypesAction: () -> Unit = {}, + startedTestGenerationAction: () -> Unit = {}, + notGeneratedTestsAction: (List) -> Unit = {}, // take names of functions without tests + processMypyWarnings: (List) -> Unit = {}, + processCoverageInfo: (String) -> Unit = {}, + startedCleaningAction: () -> Unit = {}, + finishedAction: (List) -> Unit = {}, // take names of functions with generated tests + pythonRunRoot: Path? = null + ) { + Cleaner.restart() + + try { + TemporaryFileManager.setup(testSourceRoot) + + if (!doNotCheckRequirements) { + checkingRequirementsAction() + if (!requirementsAreInstalled(pythonPath)) { + val result = requirementsAreNotInstalledAction() + if (result == MissingRequirementsActionResult.NOT_INSTALLED) + return + } + } + + startedLoadingPythonTypesAction() + PythonTypesStorage.pythonPath = pythonPath + + val onlySpecifiedFile = if (!visitOnlySpecifiedSource) null else File(pythonFilePath) + PythonTypesStorage.refreshProjectClassesAndModulesLists(directoriesForSysPath, onlySpecifiedFile) + StubFileFinder + + startedTestGenerationAction() + val startTime = System.currentTimeMillis() + + val testCaseGenerator = PythonTestCaseGenerator.apply { + init( + directoriesForSysPath, + currentPythonModule, + pythonPath, + pythonFilePath, + timeoutForRun, + withMinimization, + pythonRunRoot = pythonRunRoot ?: Path(testSourceRoot) + ) { isCanceled() || (System.currentTimeMillis() - startTime) > timeout } + } + + val tests = pythonMethods.map { method -> + testCaseGenerator.generate(method) + } + + val (notEmptyTests, emptyTestSets) = tests.partition { it.executions.isNotEmpty() } + + if (isCanceled()) + return + + if (emptyTestSets.isNotEmpty()) { + notGeneratedTestsAction(emptyTestSets.map { it.method.name }) + } + + if (notEmptyTests.isEmpty()) + return + + val classId = + if (containingClassName == null) + PythonClassId("$currentPythonModule.TopLevelFunctions") + else + PythonClassId("$currentPythonModule.$containingClassName") + + val methodIds = notEmptyTests.associate { + it.method to PythonMethodId( + classId, + it.method.name, + RawPythonAnnotation(it.method.returnAnnotation ?: pythonNoneClassId.name), + it.method.arguments.map { argument -> + argument.annotation?.let { annotation -> + RawPythonAnnotation(annotation) + } ?: pythonAnyClassId + } + ) + } + + val paramNames = notEmptyTests.associate { testSet -> + methodIds[testSet.method] as ExecutableId to testSet.method.arguments.map { it.name } + }.toMutableMap() + + + val importParamModules = notEmptyTests.flatMap { testSet -> + testSet.executions.flatMap { execution -> + execution.stateBefore.parameters.flatMap { utModel -> + (utModel as PythonModel).let { + it.allContainingClassIds.map { classId -> + PythonUserImport(classId.moduleName) + } + } + } + } + } + val importResultModules = notEmptyTests.flatMap { testSet -> + testSet.executions.mapNotNull { execution -> + if (execution.result is UtExecutionSuccess) { + (execution.result as UtExecutionSuccess).let { result -> + (result.model as PythonModel).let { + it.allContainingClassIds.map { classId -> + PythonUserImport(classId.moduleName) + } + } + } + } + else null + }.flatten() + } + val testRootModules = notEmptyTests.mapNotNull { testSet -> + methodIds[testSet.method]?.rootModuleName?.let { PythonUserImport(it) } + } + val sysImport = PythonSystemImport("sys") + val sysPathImports = relativizePaths(pythonRunRoot, directoriesForSysPath).map { PythonSysPathImport(it) } + + val testFrameworkModule = testFramework.testSuperClass?.let { PythonUserImport((it as PythonClassId).rootModuleName) } + + val allImports = ( + importParamModules + importResultModules + testRootModules + sysPathImports + listOf(testFrameworkModule, sysImport) + ).filterNotNull().toSet() + + val context = UtContext(this::class.java.classLoader) + withUtContext(context) { + val codegen = PythonCodeGenerator( + classId, + paramNames = paramNames, + testFramework = testFramework, + testClassPackageName = "", + ) + val testCode = codegen.pythonGenerateAsStringWithTestReport( + notEmptyTests.map { testSet -> + CgMethodTestSet( + methodIds[testSet.method] as ExecutableId, + testSet.executions + ) + }, + allImports + ).generatedCode + writeTestTextToFile(testCode) + } + + val coverageInfo = getCoverageInfo(notEmptyTests) + processCoverageInfo(coverageInfo) + + val mypyReport = getMypyReport(notEmptyTests, pythonFileContent) + if (mypyReport.isNotEmpty()) + processMypyWarnings(mypyReport) + + finishedAction(notEmptyTests.map { it.method.name }) + + } finally { + startedCleaningAction() + Cleaner.doCleaning() + } + } + + enum class MissingRequirementsActionResult { + INSTALLED, NOT_INSTALLED + } + + private fun getMypyReport(notEmptyTests: List, pythonFileContent: String): List = + notEmptyTests.flatMap { testSet -> + val lineOfFunction = getLineOfFunction(pythonFileContent, testSet.method.name) + val msgLines = testSet.mypyReport.mapNotNull { + if (it.file != MypyAnnotations.TEMPORARY_MYPY_FILE) + null + else if (lineOfFunction != null && it.line >= 0) + ":${it.line + lineOfFunction}: ${it.type}: ${it.message}" + else + "${it.type}: ${it.message}" + } + if (msgLines.isNotEmpty()) { + listOf("MYPY REPORT (function ${testSet.method.name})") + msgLines + } else { + emptyList() + } + } + + data class InstructionSet( + val start: Int, + val end: Int + ) + + data class CoverageInfo( + val covered: List, + val notCovered: List + ) + + private val moshi: Moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + private val jsonAdapter = moshi.adapter(CoverageInfo::class.java) + + private fun getInstructionSetList(instructions: Collection): List = + instructions.sorted().fold(emptyList()) { acc, lineNumber -> + if (acc.isEmpty()) + return@fold listOf(InstructionSet(lineNumber, lineNumber)) + val elem = acc.last() + if (elem.end + 1 == lineNumber) + acc.dropLast(1) + listOf(InstructionSet(elem.start, lineNumber)) + else + acc + listOf(InstructionSet(lineNumber, lineNumber)) + } + + private fun getCoverageInfo(testSets: List): String { + val covered = mutableSetOf() + val missed = mutableSetOf>() + testSets.forEach { testSet -> + testSet.executions.forEach inner@{ execution -> + val coverage = execution.coverage as? PythonCoverage ?: return@inner + coverage.coveredInstructions.forEach { covered.add(it.lineNumber) } + missed.add(coverage.missedInstructions.map { it.lineNumber } .toSet()) + } + } + val coveredInstructionSets = getInstructionSetList(covered) + val missedInstructionSets = + if (missed.isEmpty()) + emptyList() + else + getInstructionSetList(missed.reduce { a, b -> a intersect b }) + + return jsonAdapter.toJson(CoverageInfo(coveredInstructionSets, missedInstructionSets)) + } + + private fun relativizePaths(rootPath: Path?, paths: Set): Set = + if (rootPath != null) { + paths.map { path -> + rootPath.relativize(Path(path)).pathString + }.toSet() + } else { + paths + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt new file mode 100644 index 0000000000..c87675dc8a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/UTPythonAPI.kt @@ -0,0 +1,36 @@ +package org.utbot.python + +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import org.utbot.framework.plugin.api.UtExecution +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.python.typing.MypyAnnotations + +data class PythonArgument(val name: String, val annotation: String?) + +interface PythonMethod { + val name: String + val returnAnnotation: String? + val arguments: List + val moduleFilename: String + fun asString(): String + fun ast(): FunctionDef + val containingPythonClassId: PythonClassId? + fun methodSignature(): String = "$name(" + arguments.joinToString(", ") { + "${it.name}: ${it.annotation ?: pythonAnyClassId.name}" + } + ")" +} + +data class PythonTestSet( + val method: PythonMethod, + val executions: List, + val errors: List, + val mypyReport: List, + val classId: PythonClassId? = null, +) + +class PythonCoverage( + coveredInstructions: List, + val missedInstructions: List +): Coverage(coveredInstructions) \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt new file mode 100644 index 0000000000..f079eced95 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/ArgInfoCollector.kt @@ -0,0 +1,542 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.BinOp +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.Eq +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.Gt +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.GtE +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.Lt +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.LtE +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.NotEq +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.expr.atoms.Num +import io.github.danielnaczo.python3parser.model.expr.atoms.Str +import io.github.danielnaczo.python3parser.model.expr.comprehensions.Comprehension +import io.github.danielnaczo.python3parser.model.expr.datastructures.Dict +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.operators.Operator +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.* +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.boolops.Or +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.comparisons.* +import io.github.danielnaczo.python3parser.model.expr.operators.unaryops.* +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.forStmts.For +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.Delete +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AugAssign +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import org.apache.commons.lang3.math.NumberUtils +import org.utbot.framework.plugin.api.python.* +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedContext +import org.utbot.python.PythonMethod +import org.utbot.python.typing.PythonTypesStorage +import java.math.BigDecimal +import java.math.BigInteger + +class ArgInfoCollector(val method: PythonMethod, private val argumentTypes: List) { + open class Hint + class Type(val type: PythonClassId): Hint() + data class Method(val name: String): Hint() + data class FunctionArg(val name: String, val index: Int): Hint() + data class FunctionRet(val name: String): Hint() + data class Field(val name: String): Hint() + data class Function(val name: String): Hint() + data class ArgInfoStorage( + val types: MutableSet = mutableSetOf(), + val methods: MutableSet = mutableSetOf(), + val functionArgs: MutableSet = mutableSetOf(), + val fields: MutableSet = mutableSetOf(), + val functionRets: MutableSet = mutableSetOf() + ) { + fun toList(): List { + return listOf( + types, + methods, + functionArgs, + fields, + functionRets + ).flatten() + } + } + data class GeneralStorage( + val types: MutableList = mutableListOf(), + val functions: MutableSet = mutableSetOf(), + val fields: MutableSet = mutableSetOf(), + val methods: MutableSet = mutableSetOf() + ) { + fun toList(): List { + return listOf( + types, + functions, + fields, + methods + ).flatten() + } + } + + private val paramNames = method.arguments.mapIndexedNotNull { index, param -> + if (argumentTypes[index] == pythonAnyClassId) param.name else null + } + private val collectedValues = mutableMapOf() + private val visitor = MatchVisitor(paramNames, mutableSetOf(), GeneralStorage()) + + init { + visitor.visitFunctionDef(method.ast(), collectedValues) + } + + fun getConstants(): List = visitor.constStorage.toList() + + fun getAllGeneralHints(): List = visitor.generalStorage.toList() + + fun getAllArgHints(): Map> { + return paramNames.associateWith { argName -> (collectedValues[argName]?.toList() ?: emptyList()) } + } + + private class MatchVisitor( + private val paramNames: List, + val constStorage: MutableSet, + val generalStorage: GeneralStorage + ): ModifierVisitor>() { + + private fun namePat(): Pattern<(String) -> A, A, N> { + val names: List A, A, N>> = paramNames.map { paramName -> + map0(refl(name(equal(paramName))), paramName) + } + return names.fold(reject()) { acc, elem -> or(acc, elem) } + } + + private fun typedExpr(atom: Pattern): Pattern = + opExpr(refl(atom), refl(name(drop()))) + + private fun typedExpressionPat(): Pattern<(Type) -> A, A, Expression> { + // map must preserve order + val typeMap = linkedMapOf>( + "builtins.int" to refl(num(int())), + "builtins.float" to refl(num(drop())), + "builtins.str" to refl(str(drop())), + "builtins.bool" to or(refl(true_()), refl(false_())), + "types.NoneType" to refl(none()), + "builtins.dict" to refl(dict(drop(), drop())), + "builtins.list" to refl(list(drop())), + "builtins.set" to refl(set(drop())), + "builtins.tuple" to refl(tuple(drop())) + ) + PythonTypesStorage.builtinTypes.forEach { typeNameWithoutPrefix -> + val typeNameWithPrefix = "builtins.$typeNameWithoutPrefix" + if (typeMap.containsKey(typeNameWithPrefix)) + typeMap[typeNameWithPrefix] = or( + typeMap[typeNameWithPrefix]!!, + refl(functionCallWithoutPrefix(name(equal(typeNameWithoutPrefix)), drop())) + ) + else + typeMap[typeNameWithPrefix] = refl(functionCallWithoutPrefix(name(equal(typeNameWithoutPrefix)), drop())) + } + return typeMap.entries.fold(reject()) { acc, entry -> + or(acc, map0(typedExpr(entry.value), Type(PythonClassId(entry.key)))) + } + } + + private fun addToStorage( + paramName: String, + collection: MutableMap, + add: (ArgInfoStorage) -> Unit + ) { + val argInfoStorage = collection[paramName] ?: ArgInfoStorage() + add(argInfoStorage) + collection[paramName] = argInfoStorage + } + + private fun parseAndPutType( + collection: MutableMap, + pat: Pattern<(String) -> (Type) -> Pair?, Pair?, N>, + ast: N + ) { + parse(pat, onError = null, ast) { paramName -> { storage -> Pair(paramName, storage) } } ?.let { + addToStorage(it.first, collection) { storage -> storage.types.add(it.second) } + } + } + + private fun parseAndPutFunctionRet( + collection: MutableMap, + pat: Pattern<(String) -> (FunctionRet) -> ResFuncRet, ResFuncRet, N>, + ast: N + ) { + parse(pat, onError = null, ast) { paramName -> { storage -> Pair(paramName, storage) } } ?.let { + addToStorage(it.first, collection) { storage -> storage.functionRets.add(it.second) } + } + } + + private fun collectFunctionArg(atom: Atom, param: MutableMap) { + val argNamePatterns: List (Int) -> ResFuncArg, ResFuncArg, List>> = + paramNames.map { paramName -> + map0(anyIndexed(refl(name(equal(paramName)))), paramName) + } + val argPat: Pattern<(String) -> (Int) -> ResFuncArg, ResFuncArg, List> = + argNamePatterns.fold(reject()) { acc, elem -> or(acc, elem) } + val pat = functionCallWithPrefix( + fprefix = drop(), + fid = apply(), + farguments = arguments( + fargs = argPat, + drop(), drop(), drop() + ) + ) + parse(pat, onError = null, atom) { funcName -> { paramName -> { index -> + Pair(paramName, FunctionArg(funcName, index)) + } } } ?. let { + addToStorage(it.first, param) { storage -> storage.functionArgs.add(it.second) } + } + } + + private fun collectField(atom: Atom, param: MutableMap) { + val pat = classField( + fname = namePat<(String) -> ResField, Name>(), + fattributeId = apply() + ) + parse(pat, onError = null, atom) { paramName -> { attributeId -> + Pair(paramName, Field(attributeId)) + } } ?.let { + addToStorage(it.first, param) { storage -> storage.fields.add(it.second) } + } + } + + private fun subscriptPat(): Pattern<(String) -> A, A, Atom> = + atom( + fatomElement = namePat(), + ftrailers = first(refl(index(drop()))) + ) + + private fun collectAtomMethod(atom: Atom, param: MutableMap) { + val methodPat = classMethod( + fname = namePat<(String) -> ResMethod, Name>(), + fattributeId = apply(), + farguments = drop() + ) + val getPat = swap(map0(subscriptPat(), "__getitem__")) + val pat = or(methodPat, getPat) + parse(pat, onError = null, atom) { paramName -> { attributeId -> + Pair(paramName, Method(attributeId)) + } } ?.let { + addToStorage(it.first, param) { storage -> storage.methods.add(it.second) } + } + } + + private val magicMethodOfFunctionCall: Map = + mapOf( + "len" to "__len__", + "str" to "__str__", + "repr" to "__repr__", + "bytes" to "__bytes__", + "format" to "__format__", + "hash" to "__hash__", + "bool" to "__bool__", + "dir" to "__dir__" + ) + + private fun collectMagicMethodsFromCalls(atom: Atom, param: MutableMap) { + val callNamePat: Pattern<(String) -> (String) -> ResMethod, (String) -> ResMethod, Name> = + magicMethodOfFunctionCall.entries.fold(reject()) { acc, entry -> + or(acc, map0(name(equal(entry.key)), entry.value)) + } + val pat = functionCallWithoutPrefix( + fname = callNamePat, + farguments = arguments( + fargs = any(namePat()), + drop(), drop(), drop() + ) + ) + parse(pat, onError = null, atom) { methodName -> { paramName -> + Pair(paramName, Method(methodName)) + } } ?.let { + addToStorage(it.first, param) { storage -> storage.methods.add(it.second) } + } + } + + private fun collectFunction(atom: Atom) { + parse( + functionCallWithPrefix( + fid = apply(), + fprefix = drop(), + farguments = drop() + ), + onError = null, + atom + ) { it } ?.let { generalStorage.functions.add(Function(it)) } + } + + private fun collectGeneralMethod(atom: Atom) { + parse( + methodFromAtom( + fattributeId = apply(), + farguments = drop() + ), + onError = null, + atom + ) { it } ?.let { generalStorage.methods.add(Method(it)) } + } + + private fun collectGeneralFields(atom: Atom) { + parse( + attributesFromAtom(fattributes = apply()), + onError = null, + atom + ) { it } ?.let { attributes -> + attributes.forEach { generalStorage.fields.add(Field(it)) } + } + } + + override fun visitAtom(atom: Atom, param: MutableMap): AST { + collectFunctionArg(atom, param) + collectField(atom, param) + collectAtomMethod(atom, param) + collectMagicMethodsFromCalls(atom, param) + collectFunction(atom) + collectGeneralMethod(atom) + collectGeneralFields(atom) + return super.visitAtom(atom, param) + } + + private fun collectTypes(assign: Assign, param: MutableMap) { + val pat: Pattern<(String) -> (Type) -> ResAssign, List, Assign> = assignAll( + ftargets = allMatches(namePat()), fvalue = typedExpressionPat() + ) + parse( + pat, + onError = emptyList(), + assign + ) { paramName -> { typeStorage -> Pair(paramName, typeStorage) } } .map { + addToStorage(it.first, param) { storage -> storage.types.add(it.second) } + } + } + + private fun collectSetItem(assign: Assign, param: MutableMap) { + val setItemPat: Pattern<(String) -> String, List, Assign> = assignAll( + ftargets = allMatches(refl(subscriptPat())), + fvalue = drop() + ) + val setItemParams = parse(setItemPat, onError = emptyList(), assign) { it } + setItemParams.map { paramName -> + addToStorage(paramName, param) { storage -> + storage.methods.add(Method("__setitem__")) + } + } + } + + private fun funcCallNamePat(): Pattern<(FunctionRet) -> A, A, N> = + map1(refl(functionCallWithPrefix( + fprefix = drop(), + fid = apply(), + farguments = drop() + ))) { x -> FunctionRet(x) } + + private fun collectFuncRet(assign: Assign, param: MutableMap) { + val pat: Pattern<(String) -> (FunctionRet) -> ResFuncRet, List, Assign> = assignAll( + ftargets = allMatches(namePat()), + fvalue = funcCallNamePat() + ) + val functionRets = parse(pat, onError = emptyList(), assign) { paramName -> + { functionStorage -> Pair(paramName, functionStorage) } + } + functionRets.forEach { + if (it != null) + addToStorage(it.first, param) { storage -> storage.functionRets.add(it.second) } + } + } + + override fun visitAssign(ast: Assign, param: MutableMap): AST { + collectTypes(ast, param) + collectSetItem(ast, param) + collectFuncRet(ast, param) + return super.visitAssign(ast, param) + } + + private fun getOpMagicMethod(op: Operator?) = + when (op) { + is Gt -> "__gt__" + is GtE -> "__ge__" + is Lt -> "__lt__" + is LtE -> "__le__" + is Eq -> "__eq__" + is NotEq -> "__ne__" + is In -> "__contains__" + is FloorDiv -> "__floordiv__" + is Invert -> "__invert__" + is LShift -> "__lshift__" + is Mod -> "__mod__" + is Mult -> "__mul__" + is USub -> "__neg__" + is Or -> "__or__" + is UAdd -> "__pos__" + is Pow -> "__pow__" + is RShift -> "__rshift__" + is Sub -> "__sub__" + is Add -> "__add__" + is Div -> "__truediv__" + is BitXor -> "__xor__" + is Not -> "__not__" + else -> null + } + + override fun visitAugAssign(ast: AugAssign, param: MutableMap): AST { + parseAndPutType(param, augAssign(ftarget = namePat(), fvalue = typedExpressionPat(), fop = drop()), ast) + parseAndPutFunctionRet(param, augAssign(ftarget = namePat(), fvalue = funcCallNamePat(), fop = drop()), ast) + saveToAttributeStorage(ast.target, getOpMagicMethod(ast.op), param) + saveToAttributeStorage(ast.value, getOpMagicMethod(ast.op), param) + return super.visitAugAssign(ast, param) + } + + private fun getOp(ast: BinOp): FuzzedContext = + when (ast) { + is Eq -> FuzzedContext.Comparison.EQ + is NotEq -> FuzzedContext.Comparison.NE + is Gt -> FuzzedContext.Comparison.GT + is GtE -> FuzzedContext.Comparison.GE + is Lt -> FuzzedContext.Comparison.LT + is LtE -> FuzzedContext.Comparison.LE + else -> FuzzedContext.Unknown + } + + private fun getNumFuzzedValue(num: String, op: FuzzedContext = FuzzedContext.Unknown): FuzzedConcreteValue? = + try { + when (val x = NumberUtils.createNumber(num)) { + is Int -> FuzzedConcreteValue(pythonIntClassId, x.toBigInteger(), op) + is Long -> FuzzedConcreteValue(pythonIntClassId, x.toBigInteger(), op) + is BigInteger -> FuzzedConcreteValue(pythonIntClassId, x, op) + else -> FuzzedConcreteValue(pythonFloatClassId, BigDecimal(num), op) + } + } catch (e: NumberFormatException) { + null + } + + private fun constPat(op: FuzzedContext): Pattern<(FuzzedConcreteValue?) -> A, A, N> { + val pats = listOf A, A, N>>( + map1(refl(num(apply()))) { x -> getNumFuzzedValue(x, op) }, + map1(refl(str(apply()))) { x -> + FuzzedConcreteValue(pythonStrClassId, x, op) + }, + map0( + refl(true_()), + FuzzedConcreteValue(PythonBoolModel.classId, true, op) + ), + map0( + refl(false_()), + FuzzedConcreteValue(PythonBoolModel.classId, false, op) + ) + ) + return pats.reduce { acc, elem -> or(acc, elem) } + } + + override fun visitNum(num: Num, param: MutableMap): AST { + val value = getNumFuzzedValue(num.n) + if (value != null && constStorage.find { it.value == value.value } == null) { + constStorage.add(value) + (value.classId as? PythonClassId) ?.let { generalStorage.types.add(Type(it)) } + } + return super.visitNum(num, param) + } + + override fun visitStr(str: Str, param: MutableMap?): AST { + if (str.s.isEmpty() || str.s[0] != 'f') + constStorage.add(FuzzedConcreteValue(pythonStrClassId, str.s)) + generalStorage.types.add(Type(pythonStrClassId)) + return super.visitStr(str, param) + } + + override fun visitBinOp(ast: BinOp, param: MutableMap): AST { + parseAndPutType( + param, + or( + binOp(fleft = namePat(), fright = typedExpressionPat()), + swap(binOp(fleft = typedExpressionPat(), fright = namePat())) + ), + ast + ) + parseAndPutFunctionRet( + param, + or( + binOp(fleft = namePat(), fright = funcCallNamePat()), + swap(binOp(fleft = funcCallNamePat(), fright = namePat())) + ), + ast + ) + val op = getOp(ast) + if (op != FuzzedContext.Unknown) + parse( + binOp(fleft = refl(name(drop())), fright = constPat(op)), + onError = null, + ast + ) { it } ?.let { constStorage.add(it) } + + saveToAttributeStorage(ast.left, getOpMagicMethod(ast), param) + saveToAttributeStorage(ast.right, getOpMagicMethod(ast), param) + return super.visitBinOp(ast, param) + } + + fun saveToAttributeStorage(name: AST?, methodName: String?, param: MutableMap) { + if (methodName == null) + return + paramNames.forEach { + if (name is Name && name.id.name == it) { + addToStorage(it, param) { storage -> + storage.methods.add(Method(methodName)) + } + } + } + } + + override fun visitUnaryOp(unaryOp: UnaryOp, param: MutableMap): AST { + saveToAttributeStorage(unaryOp.expression, getOpMagicMethod(unaryOp), param) + return super.visitUnaryOp(unaryOp, param) + } + + override fun visitDelete(delete: Delete, param: MutableMap): AST { + saveToAttributeStorage(delete.expression, "__delitem__", param) + return super.visitDelete(delete, param) + } + + override fun visitListExpr(listExpr: ListExpr, param: MutableMap): AST { + generalStorage.types.add(Type(PythonListModel.classId)) + return super.visitListExpr(listExpr, param) + } + + override fun visitSet( + set: io.github.danielnaczo.python3parser.model.expr.datastructures.Set, + param: MutableMap + ): AST { + generalStorage.types.add(Type(PythonSetModel.classId)) + return super.visitSet(set, param) + } + + override fun visitDict(dict: Dict, param: MutableMap): AST { + generalStorage.types.add(Type(PythonDictModel.classId)) + return super.visitDict(dict, param) + } + + override fun visitComprehension( + comprehension: Comprehension, + param: MutableMap + ): AST { + generalStorage.methods.add(Method("__iter__")) + parse(namePat(), onError = null, comprehension.iter) { it } ?.let { paramName -> + addToStorage(paramName, param) { storage -> storage.methods.add(Method("__iter__")) } + } + return super.visitComprehension(comprehension, param) + } + + override fun visitFor(forElement: For, param: MutableMap): AST { + generalStorage.methods.add(Method("__iter__")) + parse(namePat(), onError = null, forElement.iter) { it } ?.let { paramName -> + addToStorage(paramName, param) { storage -> storage.methods.add(Method("__iter__")) } + } + return super.visitFor(forElement, param) + } + } +} + +typealias ResFuncArg = Pair? +typealias ResField = Pair? +typealias ResMethod = Pair? +typealias ResAssign = Pair +typealias ResFuncRet = Pair? \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt new file mode 100644 index 0000000000..b972e868e8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/ClassInfoCollector.kt @@ -0,0 +1,64 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import org.utbot.python.PythonMethod + +class ClassInfoCollector(pyClass: PythonClass) { + val pyClass: PythonClass = pyClass + + class Storage { + val fields = mutableSetOf() + val methods = mutableSetOf() + } + val storage = Storage() + + init { + pyClass.methods.forEach { method -> + if (isProperty(method)) + storage.fields.add(method.name) + else + storage.methods.add(method.name) + + val selfName = getSelfName(method) + if (selfName != null) { + val visitor = Visitor(selfName) + visitor.visitFunctionDef(method.ast(), storage) + } + } + pyClass.topLevelFields.forEach { annAssign -> + (annAssign.target as? Name)?.let { storage.fields.add(it.id.name) } + } + } + + companion object { + fun getSelfName(method: PythonMethod): String? { + val params = method.arguments + if (params.isEmpty() || method.ast().decorators.any { + listOf( + "staticmethod", + "classmethod" + ).contains(it.name.name) + }) return null + return params[0].name + } + fun isProperty(method: PythonMethod): Boolean { + return method.ast().decorators.any { it.name.name == "property" } + } + } + + private class Visitor(val selfName: String): ModifierVisitor() { + override fun visitAtom(atom: Atom, param: Storage): AST { + parse( + classField(fname = name(equal(selfName)), fattributeId = apply()), + onError = null, + atom + ) { it } ?.let { fieldName -> + param.fields.add(fieldName) + } + return super.visitAtom(atom, param) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt new file mode 100644 index 0000000000..99f83797c6 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/CodeGen.kt @@ -0,0 +1,422 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.Identifier +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.expr.atoms.Str +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.Attribute +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Arguments +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Keyword +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.datastructures.Tuple +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.Add +import io.github.danielnaczo.python3parser.model.mods.Module +import io.github.danielnaczo.python3parser.model.stmts.Body +import io.github.danielnaczo.python3parser.model.stmts.Statement +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameter +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameters +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.tryExceptStmts.ExceptHandler +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.tryExceptStmts.Try +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.withStmts.With +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.withStmts.WithItem +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Alias +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Import +import io.github.danielnaczo.python3parser.model.stmts.importStmts.ImportFrom +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.visitors.prettyprint.IndentationPrettyPrint +import io.github.danielnaczo.python3parser.visitors.prettyprint.ModulePrettyPrintVisitor +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.python.* +import org.utbot.python.code.AnnotationProcessor.getModulesFromAnnotation + + +object PythonCodeGenerator { + private val pythonTreeSerializerCode = PythonCodeGenerator::class.java.getResource("/python_tree_serializer.py") + ?.readText(Charsets.UTF_8) + ?: error("Didn't find preprocessed_values.json") + + private fun toString(module: Module): String { + val modulePrettyPrintVisitor = ModulePrettyPrintVisitor() + return modulePrettyPrintVisitor.visitModule(module, IndentationPrettyPrint(0)) + } + + private fun createArguments( + args: List = emptyList(), + keywords: List = emptyList(), + starredArgs: List = emptyList(), + doubleStarredArgs: List = emptyList() + ): Arguments { + return Arguments(args, keywords, starredArgs, doubleStarredArgs) + } + + private fun generateImportFunctionCode( + functionPath: String, + directoriesForSysPath: Set, + additionalModules: Set = emptySet(), + ): List { + val systemImport = Import(listOf( + Alias("sys"), + Alias("typing"), + Alias("json"), + Alias("inspect"), + Alias("builtins"), + )) + val systemCalls = directoriesForSysPath.map { path -> + Atom( + Name("sys.path.append"), + listOf( + createArguments( + listOf(Str(path)) + ) + ) + ) + } + + val additionalImport = additionalModules.map { Import(listOf(Alias(it))) } + val import = ImportFrom(functionPath, listOf(Alias("*"))) + return listOf(systemImport) + systemCalls + additionalImport + listOf(import) + } + + private fun generateFunctionCallForTopLevelFunction(method: PythonMethod): Atom { + val keywords = method.arguments.map { + Keyword(Name(it.name), Name(it.name)) + } + return Atom( + Name(method.name), + listOf( + createArguments(emptyList(), keywords) + ) + ) + } + + private fun generateMethodCall(method: PythonMethod): Atom { + assert(method.containingPythonClassId != null) + val keywords = method.arguments.drop(1).map { + Keyword(Name(it.name), Name(it.name)) + } + return Atom( + Name(method.arguments[0].name), + listOf( + Attribute(Identifier(method.name)), + createArguments(emptyList(), keywords) + ) + ) + } + + const val successStatus = "success" + const val failStatus = "fail" + + fun generateRunFunctionCode( + method: PythonMethod, + methodArguments: List, + directoriesForSysPath: Set, + moduleToImport: String, + additionalModules: Set = emptySet(), + fileForOutputName: String + ): String { + + val importStatements = generateImportFunctionCode( + moduleToImport, + directoriesForSysPath, + additionalModules + setOf("coverage") + ) + + val testFunctionName = "__run_${method.name}" + val testFunction = FunctionDef(testFunctionName) + + val parameters = methodArguments.zip(method.arguments).map { (model, argument) -> + Assign( + listOf(Name(argument.name)), + Name(model.toString()) + ) + } + + val resultName = Name("__result") + val startName = Name("__start") + val endName = Name("__end") + val sourcesName = Name("__sources") + val stmtsName = Name("__stmts") + val stmtsFilteredName = Name("__stmts_filtered") + val stmtsFilteredWithDefName = Name("__stmts_filtered_with_def") + val missedName = Name("__missed") + val missedFilteredName = Name("__missed_filtered") + val coverageName = Name("__cov") + val fullpathName = Name("__fullpath") + val statusName = Name("__status") + val exceptionName = Name("__exception") + val serialisedName = Name("__serialized") + val fileName = Name("__out_file") + + val fullpath = Assign( + listOf(fullpathName), + Str(method.moduleFilename) + ) + + val functionCall = + if (method.containingPythonClassId == null) + generateFunctionCallForTopLevelFunction(method) + else + generateMethodCall(method) + + val fullFunctionName = Name(( + listOf((functionCall.atomElement as Name).id.name) + functionCall.trailers.mapNotNull { + if (it is Attribute) { + it.attr.name + } else { + null + } + }).joinToString(".") + ) + + val coverage = Assign( + listOf(coverageName), + Name("coverage.Coverage(data_suffix=True)") + ) + val startCoverage = Atom( + coverageName, + listOf(Attribute(Identifier("start")), createArguments()) + ) + + val resultSuccess = Assign( + listOf(resultName), + functionCall + ) + + val statusSuccess = Assign( + listOf(statusName), + Str("\"" + successStatus + "\"") + ) + + val resultError = Assign( + listOf(resultName), + exceptionName + ) + + val statusError = Assign( + listOf(statusName), + Str("\"" + failStatus + "\"") + ) + + val stopCoverage = Atom( + coverageName, + listOf(Attribute(Identifier("stop")), createArguments()) + ) + val sourcesAndStart = Assign( + listOf(Tuple(listOf(sourcesName, startName))), + Atom( + Name("inspect.getsourcelines"), + listOf(createArguments(listOf(fullFunctionName))) + ) + ) + val end = Assign( + listOf(endName), + Add( + startName, + Atom(Name("len"), listOf(createArguments(listOf(sourcesName)))) + ) + ) + val covAnalysis = Assign( + listOf(Tuple(listOf( + Name("_"), + stmtsName, + Name("_"), + missedName, + Name("_") + ))), + Atom( + coverageName, + listOf( + Attribute(Identifier("analysis2")), + createArguments(listOf(fullpathName)) + ) + ) + ) + val clean = Atom( + coverageName, + listOf(Attribute(Identifier("erase")), createArguments()) + ) + val stmtsFiltered = Assign( + listOf(stmtsFilteredName), + Atom( + Name(getLinesName), + listOf(createArguments(listOf(startName, endName, stmtsName))) + ) + ) + val stmtsFilteredWithDef = Assign( + listOf(stmtsFilteredWithDefName), + Add( + ListExpr(listOf(startName)), + stmtsFilteredName + ) + ) + val missedFiltered = Assign( + listOf(missedFilteredName), + Atom( + Name(getLinesName), + listOf(createArguments(listOf(startName, endName, missedName))) + ) + ) + + val serialize = Assign( + listOf(serialisedName), + Atom( + Name("_PythonTreeSerializer().dumps"), + listOf(createArguments(listOf(resultName))) + ) + ) + + val jsonDumps = Atom( + Name("json"), + listOf( + Attribute(Identifier("dumps")), + createArguments(listOf(serialisedName)) + ) + ) + + val printStmt = With( + listOf( + WithItem(Name("open(\"$fileForOutputName\", \"w\")"), fileName) + ), + Atom( + fileName, + listOf( + Attribute(Identifier("write")), + createArguments( + listOf( + Atom( + Name("\"\\n\""), + listOf( + Attribute(Identifier("join")), + createArguments(listOf( + ListExpr( + listOf( + Atom(Name("str"), listOf(createArguments(listOf(statusName)))), + Atom(Name("str"), listOf(createArguments(listOf(jsonDumps)))), + Atom(Name("str"), listOf(createArguments(listOf(stmtsFilteredWithDefName)))), + Atom(Name("str"), listOf(createArguments(listOf(missedFilteredName)))) + ) + ) + )) + ) + ) + ) + ) + ) + ) + ) + + val tryBody = Body(listOf( + resultSuccess, + statusSuccess + )) + val suppressedBlock = With( + listOf(WithItem(Atom( + Name(getStdoutSuppressName), + listOf(createArguments()) + ))), + tryBody + ) + val failBody = Body(listOf( + resultError, + statusError + )) + val tryHandler = ExceptHandler("Exception", exceptionName.id.name) + val tryBlock = Try(suppressedBlock, listOf(tryHandler), listOf(failBody)) + + (parameters + listOf( + fullpath, + coverage, + startCoverage + )).forEach { testFunction.addStatement(it) } + + testFunction.addStatement(tryBlock) + + listOf( + stopCoverage, + sourcesAndStart, + end, + covAnalysis, + clean, + stmtsFiltered, + stmtsFilteredWithDef, + missedFiltered, + serialize, + printStmt + ).forEach { testFunction.addStatement(it) } + + val runFunction = Atom( + Name(testFunctionName), + listOf(createArguments()) + ) + + return listOf( + getStdoutSuppress, + pythonTreeSerializerCode, + getLines, + toString( + Module( + importStatements + listOf(testFunction, runFunction) + ) + ) + ).joinToString("\n\n") + } + + fun generateMypyCheckCode( + method: PythonMethod, + methodAnnotations: Map, + directoriesForSysPath: Set, + moduleToImport: String + ): String { + val importStatements = generateImportFunctionCode( + moduleToImport, + directoriesForSysPath, + methodAnnotations.values.flatMap { annotation -> + getModulesFromAnnotation(annotation) + }.toSet(), + ) + + val parameters = Parameters( + method.arguments.map { argument -> + Parameter("${argument.name}: ${methodAnnotations[argument.name] ?: pythonAnyClassId.name}") + }, + ) + + val testFunctionName = "__mypy_check_${method.name}" + val testFunction = FunctionDef( + testFunctionName, + parameters, + method.ast().body + ) + + return toString( + Module( + importStatements + listOf(testFunction) + ) + ) + } + + private const val getLinesName: String = "__get_lines" + private val getLines: String = """ + def ${this.getLinesName}(start, end, lines): + return list(filter(lambda x: start < x < end, lines)) + """.trimIndent() + + private const val getStdoutSuppressName: String = "__suppress_stdout" + private val getStdoutSuppress: String = """ + import os + from contextlib import contextmanager + @contextmanager + def ${this.getStdoutSuppressName}(): + with open(os.devnull, "w") as devnull: + old_stdout = sys.stdout + sys.stdout = devnull + try: + yield + finally: + sys.stdout = old_stdout + """.trimIndent() +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt new file mode 100644 index 0000000000..6b63ccb787 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/KlaxonPythonTreeParser.kt @@ -0,0 +1,102 @@ +package org.utbot.python.code + +import com.beust.klaxon.JsonArray +import com.beust.klaxon.JsonObject +import com.beust.klaxon.Parser +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.PythonTree + +class KlaxonPythonTreeParser( + jsonString: String +) { + private val jsonObject = parseJsonString(jsonString) + private val rawMemory = jsonObject.obj("memory")!!.map { + it.key.toLong() to it.value as JsonObject + }.toMap() + private val memory = emptyMap().toMutableMap() + + fun parseJsonToPythonTree(): PythonTree.PythonTreeNode { + return parseToPythonTree(jsonObject.obj("json")!!) + } + + private fun parseJsonString(jsonString: String): JsonObject { + val parser: Parser = Parser.default() + val stringBuilder: StringBuilder = StringBuilder(jsonString) + return parser.parse(stringBuilder) as JsonObject + } + + private fun findInMemory(id: Long): PythonTree.PythonTreeNode { + return if (memory.containsKey(id)) + memory[id]!! + else { + return parseReduce(rawMemory[id]!!) + } + } + + private fun parseToPythonTree(json: JsonObject): PythonTree.PythonTreeNode { + val type = json.string("type")!! + val strategy = json.string("strategy")!! + val comparable = json.boolean("comparable")!! + + val result = if (strategy == "repr") { + var repr = json.string("value")!! + if (type == "builtins.complex") { + repr = "complex('$repr')" + } else if (repr == "nan") { + repr = "float('$repr')" + } else if (repr == "inf") { + repr = "float('$repr')" + } else if (repr == "-inf") { + repr = "float('$repr')" + } + PythonTree.PrimitiveNode(PythonClassId(type), repr) + } else { + when (type) { + "builtins.list" -> parsePythonList(json.array("value")!!) + "builtins.set" -> parsePythonSet(json.array("value")!!) + "builtins.tuple" -> parsePythonTuple(json.array("value")!!) + "builtins.dict" -> parsePythonDict(json.array("value")!!) + else -> findInMemory(json.long("value")!!) + } + } + result.comparable = comparable + return result + } + + private fun parseReduce(value: JsonObject): PythonTree.PythonTreeNode { + val id = value.long("id")!! + val initObject = PythonTree.ReduceNode( + id, + PythonClassId(value.string("type")!!), + PythonClassId(value.string("constructor")!!), + parsePythonList(value.array("args")!!).items, + ) + memory[id] = initObject + initObject.state = parsePythonDict(value.array("state")!!).items.map { + (it.key as PythonTree.PrimitiveNode).repr to it.value + }.toMap() + initObject.listitems = parsePythonList(value.array("listitems")!!).items + initObject.dictitems = parsePythonDict(value.array("dictitems")!!).items + return initObject + } + + private fun parsePythonList(items: JsonArray): PythonTree.ListNode { + return PythonTree.ListNode(items.map { parseToPythonTree(it) }) + } + + private fun parsePythonSet(items: JsonArray): PythonTree.SetNode { + return PythonTree.SetNode(items.map { parseToPythonTree(it) }.toSet()) + } + + private fun parsePythonTuple(items: JsonArray): PythonTree.TupleNode { + return PythonTree.TupleNode(items.map { parseToPythonTree(it) }) + } + + private fun parsePythonDict(items: JsonArray>): PythonTree.DictNode { + return PythonTree.DictNode(items.associate { + val key = it[0] + val value = it[1] as JsonObject + (if (key is String) PythonTree.PrimitiveNode(PythonClassId("builtins.str"), key) else parseToPythonTree(key as JsonObject)) to parseToPythonTree(value) + }) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt new file mode 100644 index 0000000000..2f0306860c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonASTParser.kt @@ -0,0 +1,420 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.* +import io.github.danielnaczo.python3parser.model.expr.datastructures.Dict +import io.github.danielnaczo.python3parser.model.expr.datastructures.Set +import io.github.danielnaczo.python3parser.model.expr.datastructures.ListExpr +import io.github.danielnaczo.python3parser.model.expr.datastructures.Tuple +import io.github.danielnaczo.python3parser.model.expr.operators.Operator +import io.github.danielnaczo.python3parser.model.expr.operators.binaryops.BinOp +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.Assign +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AugAssign +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Arguments +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.arguments.Keyword +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.Attribute +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.subscripts.Index +import io.github.danielnaczo.python3parser.model.expr.atoms.trailers.subscripts.Subscript +import io.github.danielnaczo.python3parser.model.expr.operators.unaryops.UnaryOp +import org.apache.commons.lang3.math.NumberUtils +import java.math.BigInteger + +sealed class Result +class Match(val value: T): Result() +class Error : Result() + +open class Pattern ( + val go: (N, A) -> Result +) + +fun parse(pat: Pattern, onError: B, node: N, x: A): B { + return when (val result = pat.go(node, x)) { + is Match -> result.value + is Error -> onError + } +} + +inline fun refl(pat: Pattern): Pattern = + Pattern { node, x -> + when (node) { + is N -> pat.go(node, x) + else -> Error() + } + } + +fun drop(): Pattern = + Pattern { _, x -> Match(x) } + +fun apply(): Pattern<(N) -> A, A, N> = + Pattern { node, x -> Match(x(node)) } + +fun name(fid: Pattern): Pattern = + Pattern { node, x -> + fid.go(node.id.name, x) + } + +fun int(): Pattern = + Pattern { node, x -> + when (NumberUtils.createNumber(node)) { + is Int -> Match(x) + is Long -> Match(x) + is BigInteger -> Match(x) + else -> Error() + } + } + +fun num(fnum: Pattern): Pattern = Pattern { node, x -> fnum.go(node.n, x) } +fun str(fstr: Pattern): Pattern = Pattern { node, x -> fstr.go(node.s, x) } +fun true_(): Pattern = Pattern { _, x -> Match(x) } +fun false_(): Pattern = Pattern { _, x -> Match(x) } +fun none(): Pattern = Pattern { _, x -> Match(x) } + +fun equal(value: N): Pattern = + Pattern { node, x -> + if (node != value) + Error() + else + Match(x) + } + +fun go(y: Pattern, node: N, x: Result) = + when (x) { + is Match -> y.go(node, x.value) + is Error -> Error() + } + +fun toMatch(x: Result>): Match> = + when (x) { + is Error -> Match(emptyList()) + is Match -> x + } + +fun multiGo(y: Pattern, node: N, x: Result>): Result> { + val res = mutableListOf() + for (z in (toMatch(x)).value) { + when (val w = y.go(node, z)) { + is Error -> Unit + is Match -> res.add(w.value) + } + } + return Match(res) +} + +fun assign( + ftargets: Pattern>, + fvalue: Pattern +): Pattern = + Pattern { node, x -> + if (!node.value.isPresent) + return@Pattern Error() + val x1 = ftargets.go(node.targets, x) + go(fvalue, node.value.get(), x1) + } + +fun assignAll( + ftargets: Pattern, List>, + fvalue: Pattern +): Pattern, Assign> = + Pattern { node, x -> + if (!node.value.isPresent) + return@Pattern Error() + val x1 = ftargets.go(node.targets, x) + multiGo(fvalue, node.value.get(), x1) + } + +fun dict( + fkeys: Pattern>, + fvalues: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fkeys.go(node.keys, x) + go(fvalues, node.values, x1) + } + +fun list( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun set( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun tuple( + felts: Pattern> +): Pattern = + Pattern { node, x -> + felts.go(node.elts, x) + } + +fun augAssign( + ftarget: Pattern, + fop: Pattern, + fvalue: Pattern +): Pattern = + Pattern { node, x -> + val x1 = ftarget.go(node.target, x) + val x2 = go(fop, node.op, x1) + go(fvalue, node.value, x2) + } + +fun binOp( + fleft: Pattern, + fright: Pattern +): Pattern = + Pattern { node, x -> + if (node.left == null || node.right == null) + return@Pattern Error() + val x1 = fleft.go(node.left, x) + go(fright, node.right, x1) + } + +fun arguments( + fargs: Pattern>, + fkeywords: Pattern>, + fstarredArgs: Pattern>, + fdoubleStarredArgs: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fargs.go(node.args ?: emptyList(), x) + val x2 = go(fkeywords, node.keywords ?: emptyList(), x1) + val x3 = go(fstarredArgs, node.starredArgs ?: emptyList(), x2) + go(fdoubleStarredArgs, node.doubleStarredArgs ?: emptyList(), x3) + } + +fun atom( + fatomElement: Pattern, + ftrailers: Pattern> +): Pattern = + Pattern { node, x -> + val x1 = fatomElement.go(node.atomElement, x) + go(ftrailers, node.trailers, x1) + } + +fun list1( + felem1: Pattern +): Pattern> = + Pattern { node, x -> + if (node.size != 1) + return@Pattern Error() + felem1.go(node[0], x) + } + +fun list2( + felem1: Pattern, + felem2: Pattern +): Pattern> = + Pattern { node, x -> + if (node.size != 2) + return@Pattern Error() + val x1 = felem1.go(node[0], x) + go(felem2, node[1], x1) + } + +fun first( + felem: Pattern +): Pattern> = + Pattern { node, x -> + if (node.isEmpty()) + return@Pattern Error() + felem.go(node[0], x) + } + +fun index( + felem: Pattern +): Pattern = + Pattern { node, x -> + if (node.slice !is Index) + return@Pattern Error() + felem.go((node.slice as Index).value, x) + } + +fun attribute( + fattributeId: Pattern +): Pattern = + Pattern { node, x -> fattributeId.go(node.attr.name, x) } + +fun classField( + fname: Pattern, + fattributeId: Pattern +): Pattern = + atom(refl(fname), list1(refl(attribute(fattributeId)))) + +fun attributesFromAtom( + fattributes: Pattern> +): Pattern = + Pattern { node, x -> + val res = mutableListOf() + for (elem in node.trailers) { + if (elem is Attribute) + res.add(elem.attr.name) + } + fattributes.go(res, x) + } + +fun classMethod( + fname: Pattern, + fattributeId: Pattern, + farguments: Pattern +): Pattern = + atom(refl(fname), list2(refl(attribute(fattributeId)), refl(farguments))) + +fun methodFromAtom( + fattributeId: Pattern, + farguments: Pattern +): Pattern = + Pattern { node, x -> + if (node.trailers.size < 2 || node.trailers.last() !is Arguments) + return@Pattern Error() + val methodName = node.trailers[node.trailers.size - 2] + if (methodName !is Attribute) + return@Pattern Error() + val x1 = fattributeId.go(methodName.attr.name, x) + go(farguments, node.trailers.last() as Arguments, x1) + } + +fun nameWithPrefixFromAtom( + fname: Pattern +): Pattern = + Pattern { node, x -> + if (node.atomElement !is Name) + return@Pattern Error() + var res = (node.atomElement as Name).id.name + for (elem in node.trailers) { + if (elem !is Attribute) + break + res += "." + elem.attr.name + } + fname.go(res, x) + } + +fun functionCallWithoutPrefix( + fname: Pattern, + farguments: Pattern +): Pattern = + atom(refl(fname), list1(refl(farguments))) + +fun functionCallWithPrefix( + fprefix: Pattern>, + fid: Pattern, + farguments: Pattern +): Pattern = + Pattern { node, x -> + if (node.trailers.size == 0) + return@Pattern Error() + if (node.trailers.size == 1) { + val x1 = fprefix.go(emptyList(), x) + return@Pattern go(functionCallWithoutPrefix(name(fid), farguments), node, x1) + } + val prefix = listOf(node.atomElement) + node.trailers.dropLast(2) + val x1 = fprefix.go(prefix, x) + val x2 = go(refl(attribute(fid)), node.trailers[node.trailers.size - 2], x1) + go(refl(farguments), node.trailers.last(), x2) + } + +fun goWithMatches( + pat: (N, A) -> Result>, + node: N, + x: Result> +): Result> = + when (x) { + is Error -> Error() + is Match -> { + when (val x1 = pat(node, x.value.first)) { + is Error -> Error() + is Match -> Match(Pair(x1.value.first, x1.value.second + x.value.second)) + } + } + } + +fun opExpr( + fatomMatch: Pattern, + fatomExtra: Pattern +): Pattern { + fun innerGo(node: Expression, x: A): Result> { + val y = fatomMatch.go(node, x) + if (y is Match) + return Match(Pair(y.value, 1)) + + val z = fatomExtra.go(node, x) + if (z is Match) + return Match(Pair(z.value, 0)) + + return when (node) { + is BinOp -> { + val x1 = innerGo(node.left, x) + goWithMatches(::innerGo, node.right, x1) + } + is UnaryOp -> innerGo(node.expression, x) + else -> Error() + } + } + return Pattern { node, x -> + when (val x1 = innerGo(node, x)) { + is Error -> Error() + is Match -> if (x1.value.second == 0) Error() else Match(x1.value.first) + } + } +} + +fun any(felem: Pattern): Pattern> = + Pattern { node, x -> + for (elem in node) { + val x1 = felem.go(elem, x) + if (x1 is Match) + return@Pattern x1 + } + return@Pattern Error() + } + +fun allMatches(felem: Pattern): Pattern, List> = + Pattern { node, x -> + val res = mutableListOf() + for (elem in node) { + val x1 = felem.go(elem, x) + if (x1 is Match) + res.add(x1.value) + } + Match(res) + } + +fun anyIndexed(felem: Pattern): Pattern<(Int) -> A, B, List> = + Pattern { node, x -> + node.forEachIndexed { index, elem -> + val x1 = felem.go(elem, x(index)) + if (x1 is Match) + return@Pattern x1 + } + return@Pattern Error() + } + +fun or(pat1: Pattern, pat2: Pattern): Pattern = + Pattern { node, x -> + val x1 = pat1.go(node, x) + when (x1) { + is Match -> x1 + is Error -> pat2.go(node, x) + } + } + +fun reject(): Pattern = Pattern { _, _ -> Error() } + +fun map0(pat: Pattern, value: C): Pattern<(C) -> A, B, N> = + Pattern { node, x: (C) -> A -> + pat.go(node, x(value)) + } + +fun map1(pat: Pattern<(A) -> B, C, N>, f: (A) -> D): Pattern<(D) -> B, C, N> = + Pattern { node, x: (D) -> B -> + pat.go(node) { y -> x(f(y)) } + } + +fun swap(pat: Pattern<(A) -> (B) -> C, D, N>): Pattern<(B) -> (A) -> C, D, N> = + Pattern { node, f: (B) -> (A) -> C -> + pat.go(node) { x -> { y -> f(y)(x) } } + } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt new file mode 100644 index 0000000000..1a974714bd --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/code/PythonCodeAPI.kt @@ -0,0 +1,188 @@ +package org.utbot.python.code + +import io.github.danielnaczo.python3parser.Python3Lexer +import io.github.danielnaczo.python3parser.Python3Parser +import io.github.danielnaczo.python3parser.model.AST +import io.github.danielnaczo.python3parser.model.expr.Expression +import io.github.danielnaczo.python3parser.model.expr.atoms.Atom +import io.github.danielnaczo.python3parser.model.expr.atoms.Name +import io.github.danielnaczo.python3parser.model.mods.Module +import io.github.danielnaczo.python3parser.model.stmts.Body +import io.github.danielnaczo.python3parser.model.stmts.Statement +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.ClassDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.FunctionDef +import io.github.danielnaczo.python3parser.model.stmts.compoundStmts.functionStmts.parameters.Parameter +import io.github.danielnaczo.python3parser.model.stmts.importStmts.Import +import io.github.danielnaczo.python3parser.model.stmts.importStmts.ImportFrom +import io.github.danielnaczo.python3parser.model.stmts.smallStmts.assignStmts.AnnAssign +import io.github.danielnaczo.python3parser.visitors.ast.ModuleVisitor +import io.github.danielnaczo.python3parser.visitors.modifier.ModifierVisitor +import io.github.danielnaczo.python3parser.visitors.prettyprint.IndentationPrettyPrint +import io.github.danielnaczo.python3parser.visitors.prettyprint.ModulePrettyPrintVisitor +import org.antlr.v4.runtime.CharStreams.fromString +import org.antlr.v4.runtime.CommonTokenStream +import org.utbot.python.* +import org.utbot.python.utils.moduleOfType +import java.util.* +import mu.KotlinLogging +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonClassId + +private val logger = KotlinLogging.logger {} + +class PythonCode(private val body: Module, val filename: String, val pythonModule: String? = null) { + fun getToplevelFunctions(): List = + body.functionDefs.mapNotNull { functionDef -> + PythonMethodBody(functionDef, filename) + } + + fun getToplevelClasses(): List = + body.classDefs?.mapNotNull { classDef -> + PythonClass(classDef, filename, pythonModule) + } ?: emptyList() + + fun getToplevelModules(): List = + body.statements?.flatMap { statement -> + when (statement) { + is Import -> statement.names.map { it.name.name } + is ImportFrom -> { + try { + listOf(statement.module.get().name) + } catch (e: NoSuchElementException) { + emptyList() + } + } + else -> emptyList() + } + }?.toSet()?.map { + PythonModule(it) + } ?: emptyList() + + companion object { + fun getFromString(code: String, filename: String, pythonModule: String? = null): PythonCode? { + logger.debug("Parsing file $filename") + return try { + val ast = textToModule(code) + if (ast.statements == null) null else PythonCode(ast, filename, pythonModule) + } catch (_: Exception) { + null + } + } + } +} + +data class PythonModule( + val name: String, +) + +class PythonClass(private val ast: ClassDef, val filename: String? = null, val pythonModule: String? = null) { + val name: String + get() = ast.name.name + + val methods: List + get() = ast.functionDefs?.map { + PythonMethodBody(it, filename ?: "", pythonClassId) + } ?: emptyList() + + val pythonClassId: PythonClassId? + get() = pythonModule?.let { PythonClassId("$it.$name") } + + val initSignature: List? + get() { + val ordinary = ast.functionDefs?.find { it.name.name == "__init__" } ?.let { + PythonMethodBody(it, filename ?: "") + } + if (ordinary != null) { + return ordinary.arguments.drop(1) // drop 'self' parameter + } + if (ast.decorators.any { it.name.name == "dataclass" }) { + return topLevelFields.map { + PythonArgument( + (it.target as? Name)?.id?.name ?: return null, + astToString(it.annotation).trim() + ) + } + } + if (ast.decorators.isEmpty() && (ast.arguments == null || !ast.arguments.isPresent)){ + return emptyList() + } + return null + } + + val topLevelFields: List + get() = (ast.body as? Body)?.statements?.mapNotNull { it as? AnnAssign } ?: emptyList() +} + +class PythonMethodBody( + private val ast: FunctionDef, + override val moduleFilename: String = "", + override val containingPythonClassId: PythonClassId? = null +): PythonMethod { + override val name: String + get() = ast.name.name + + override val returnAnnotation: String? + get() = annotationToString(ast.returns) + + // TODO: consider cases of default and named arguments + private val getParams: List = + if (ast.parameters.isPresent) ast.parameters.get().params ?: emptyList() else emptyList() + + override val arguments: List + get() = getParams.map { param -> + PythonArgument( + param.parameterName.name, + annotationToString(param.annotation) + ) + } + + override fun asString(): String { + return astToString(ast) + } + + override fun ast(): FunctionDef { + return ast + } + + companion object { + fun annotationToString(annotation: Optional): String? = + if (annotation.isPresent) astToString(annotation.get()).trim() else null + } +} + +fun astToString(stmt: Statement): String { + val modulePrettyPrintVisitor = ModulePrettyPrintVisitor() + return modulePrettyPrintVisitor.visitModule(Module(listOf(stmt)), IndentationPrettyPrint(0)) +} + +fun textToModule(code: String): Module { + val lexer = Python3Lexer(fromString(code + "\n")) + val tokens = CommonTokenStream(lexer) + val parser = Python3Parser(tokens) + val moduleVisitor = ModuleVisitor() + return moduleVisitor.visit(parser.file_input()) as Module +} + +object AnnotationProcessor { + fun getModulesFromAnnotation(annotation: NormalizedPythonAnnotation): Set { + val annotationAST = textToModule(annotation.name) + val visitor = Visitor() + val result = mutableSetOf() + visitor.visitModule(annotationAST, result) + return result + } + + private class Visitor: ModifierVisitor>() { + override fun visitAtom(atom: Atom, param: MutableSet): AST { + parse( + nameWithPrefixFromAtom(apply()), + onError = null, + atom + ) { it } ?.let { typeName -> + moduleOfType(typeName)?.let { param.add(it) } + } + + return super.visitAtom(atom, param) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt new file mode 100644 index 0000000000..fc29f4bb0b --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt @@ -0,0 +1,61 @@ +package org.utbot.python.framework.codegen + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodeGenLanguage +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.constructor.name.PythonCgNameGenerator +import org.utbot.python.framework.codegen.model.constructor.tree.* +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonRenderer + +object PythonCodeLanguage : CodeGenLanguage() { + override val displayName: String = "Python" + override val id: String = "Python" + + override val extension: String + get() = ".py" + + override val languageKeywords: Set = setOf( + "True", "False", "None", "and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del", "elif", "else", + "except", "finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "nonlocal", "not", + "or", "pass", "raise", "return", "try", "while", "with", "yield", "list", "int", "str", "float", "bool", "bytes", "frozenset", + "dict", "set", "tuple", + "abs", "aiter", "all", "any", "anext", "ascii", "bool", "breakpoint", "bytearray", "callable", "chr", "classmethod", "compile", + "complex", "delattr", "dir", "divmod", "enumerate", "eval", "exec", "filter", "format", "getattr", "globals", "hasattr", + "hash", "help", "hex", "id", "input", "isinstance", "issubclass", "iter", "len", "list", "locals", "map", "max", + "memoryview", "min", "next", "object", "oct", "open", "ord", "pow", "print", "property", "range", "repr", "reversed", + "round", "set", "setattr", "slice", "sorted", "staticmethod", "sum", "super", "type", "vars", "zip", "self" + ) + + override fun testClassName( + testClassCustomName: String?, + testClassPackageName: String, + classUnderTest: ClassId + ): Pair { + val simpleName = testClassCustomName ?: "Test${classUnderTest.simpleName}" + return Pair(simpleName, simpleName) + } + + override fun getNameGeneratorBy(context: CgContext) = PythonCgNameGenerator(context) + override fun getCallableAccessManagerBy(context: CgContext) = PythonCgCallableAccessManagerImpl(context) + override fun getStatementConstructorBy(context: CgContext) = PythonCgStatementConstructorImpl(context) + override fun getVariableConstructorBy(context: CgContext) = PythonCgVariableConstructor(context) + override fun getMethodConstructorBy(context: CgContext) = PythonCgMethodConstructor(context) + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgPythonRenderer(context, printer) + + override val testFrameworks = listOf(Unittest, Pytest) + + override fun managerByFramework(context: CgContext): TestFrameworkManager = when (context.testFramework) { + is Unittest -> UnittestManager(context) + is Pytest -> PytestManager(context) + else -> throw UnsupportedOperationException("Incorrect TestFramework ${context.testFramework}") + } + + override val defaultTestFramework = Unittest + +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt new file mode 100644 index 0000000000..8ee097328a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt @@ -0,0 +1,74 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.* +import org.utbot.framework.codegen.model.CodeGenerator +import org.utbot.framework.codegen.model.TestsCodeWithTestReport +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.python.framework.codegen.PythonCodeLanguage +import org.utbot.python.framework.codegen.model.constructor.tree.PythonCgTestClassConstructor + +class PythonCodeGenerator( + classUnderTest: ClassId, + paramNames: MutableMap> = mutableMapOf(), + testFramework: TestFramework = TestFramework.defaultItem, + mockFramework: MockFramework? = MockFramework.defaultItem, + staticsMocking: StaticsMocking = StaticsMocking.defaultItem, + forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, + generateWarningsForStaticMocking: Boolean = true, + codegenLanguage: CodegenLanguage = CodegenLanguage.defaultItem, + parameterizedTestSource: ParametrizedTestSource = ParametrizedTestSource.defaultItem, + runtimeExceptionTestsBehaviour: RuntimeExceptionTestsBehaviour = RuntimeExceptionTestsBehaviour.defaultItem, + hangingTestsTimeout: HangingTestsTimeout = HangingTestsTimeout(), + enableTestsTimeout: Boolean = true, + testClassPackageName: String = classUnderTest.packageName +) : CodeGenerator( + classUnderTest, + paramNames, + testFramework, + mockFramework, + staticsMocking, + forceStaticMocking, + generateWarningsForStaticMocking, + codegenLanguage, + parameterizedTestSource, + runtimeExceptionTestsBehaviour, + hangingTestsTimeout, + enableTestsTimeout, + testClassPackageName +) { + override var context: CgContext = CgContext( + classUnderTest = classUnderTest, + paramNames = paramNames, + testFramework = testFramework, + mockFramework = mockFramework ?: MockFramework.MOCKITO, + codegenLanguage = codegenLanguage, + codeGenLanguage = PythonCodeLanguage, + parametrizedTestSource = parameterizedTestSource, + staticsMocking = staticsMocking, + forceStaticMocking = forceStaticMocking, + generateWarningsForStaticMocking = generateWarningsForStaticMocking, + runtimeExceptionTestsBehaviour = runtimeExceptionTestsBehaviour, + hangingTestsTimeout = hangingTestsTimeout, + enableTestsTimeout = enableTestsTimeout, + testClassPackageName = testClassPackageName + ) + + fun pythonGenerateAsStringWithTestReport( + cgTestSets: List, + importModules: Set, + testClassCustomName: String? = null, + ): TestsCodeWithTestReport = withCustomContext(testClassCustomName) { + context.withTestClassFileScope { + val testClassModel = TestClassModel(classUnderTest, cgTestSets) + context.collectedImports.addAll(importModules) + val testClassFile = PythonCgTestClassConstructor(context).construct(testClassModel) + TestsCodeWithTestReport(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt new file mode 100644 index 0000000000..01360e879a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt @@ -0,0 +1,85 @@ +package org.utbot.python.framework.codegen.model + +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.framework.plugin.api.python.pythonNoneClassId +import org.utbot.framework.plugin.api.util.methodId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.plugin.api.util.voidClassId + +object Pytest : TestFramework(displayName = "pytest") { + override val mainPackage: String = "pytest" + override val assertionsClass: ClassId = pythonNoneClassId + override val arraysAssertionsClass: ClassId = assertionsClass + override val testAnnotation: String + get() = TODO("Not yet implemented") + override val testAnnotationId: ClassId = BuiltinClassId( + name = "pytest", + canonicalName = "pytest", + simpleName = "Tests" + ) + override val testAnnotationFqn: String = "" + + override val parameterizedTestAnnotation: String = "" + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val parameterizedTestAnnotationFqn: String = "" + override val methodSourceAnnotation: String = "" + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationFqn: String = "" + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + @OptIn(ExperimentalStdlibApi::class) + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List = buildList { + add(executionInvoke) + addAll(additionalArguments) + add(mainPackage) + } +} + +object Unittest : TestFramework(displayName = "Unittest") { + override val testSuperClass: ClassId = PythonClassId("unittest.TestCase") + override val mainPackage: String = "unittest" + override val assertionsClass: ClassId = PythonClassId("self") + override val arraysAssertionsClass: ClassId = assertionsClass + override val testAnnotation: String = "" + override val testAnnotationId: ClassId = BuiltinClassId( + name = "Unittest", + canonicalName = "Unittest", + simpleName = "Tests" + ) + override val testAnnotationFqn: String = "unittest" + + override val parameterizedTestAnnotation: String = "" + override val parameterizedTestAnnotationId: ClassId = pythonAnyClassId + override val parameterizedTestAnnotationFqn: String = "" + override val methodSourceAnnotation: String = "" + override val methodSourceAnnotationId: ClassId = pythonAnyClassId + override val methodSourceAnnotationFqn: String = "" + override val nestedClassesShouldBeStatic: Boolean = false + override val argListClassId: ClassId = pythonAnyClassId + + override fun getRunTestsCommand( + executionInvoke: String, + classPath: String, + classesNames: List, + buildDirectory: String, + additionalArguments: List + ): List { + throw UnsupportedOperationException() + } + + override val assertEquals by lazy { assertionId("assertEqual", objectClassId, objectClassId) } + + override fun assertionId(name: String, vararg params: ClassId): MethodId = + methodId(assertionsClass, name, voidClassId, *params) +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt new file mode 100644 index 0000000000..3c75a8d971 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonImports.kt @@ -0,0 +1,86 @@ +package org.utbot.framework.codegen + +sealed class PythonImport(order: Int): Import(order) { + var importName: String = "" + var moduleName: String? = null + + constructor(order: Int, importName: String, moduleName: String? = null): this(order) { + this.importName = importName + this.moduleName = moduleName + } + + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + val rootModuleName: String + get() = qualifiedName.split(".")[0] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSysPathImport(val sysPath: String): PythonImport(2) { + override val qualifiedName: String + get() = sysPath + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSysPathImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + return sysPath.hashCode() + } +} + +data class PythonUserImport(val importName_: String, val moduleName_: String? = null): PythonImport(3, importName_, moduleName_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonUserImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} + +data class PythonSystemImport(val importName_: String, val moduleName_: String? = null): PythonImport(1, importName_, moduleName_) { + override val qualifiedName: String + get() = if (moduleName != null) "${moduleName}.${importName}" else importName + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as PythonSystemImport + return qualifiedName == other.qualifiedName + } + + override fun hashCode(): Int { + var result = importName.hashCode() + result = 31 * result + (moduleName?.hashCode() ?: 0) + return result + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt new file mode 100644 index 0000000000..2bd0455de2 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/name/PythonCgNameGenerator.kt @@ -0,0 +1,57 @@ +package org.utbot.python.framework.codegen.model.constructor.name + +import org.utbot.framework.codegen.PythonImport +import org.utbot.framework.codegen.isLanguageKeyword +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.util.toSnakeCase + +class PythonCgNameGenerator(context_: CgContext): CgNameGeneratorImpl(context_) { + + override fun nameFrom(id: ClassId): String = + when (id) { + is NormalizedPythonAnnotation -> "var" + else -> id.simpleName.toSnakeCase() + } + + override fun variableName(type: ClassId, base: String?, isMock: Boolean): String { + val baseName = base?.toSnakeCase() ?: nameFrom(type) + val importedModuleNames = collectedImports.mapNotNull { + if (it is PythonImport) it.rootModuleName else null + } + return when { + baseName in existingVariableNames -> nextIndexedVarName(baseName) + baseName in importedModuleNames -> nextIndexedVarName(baseName) + isLanguageKeyword(baseName, context.codeGenLanguage) -> createNameFromKeyword(baseName) + else -> baseName + }.also { + existingVariableNames = existingVariableNames.add(it) + } + } + + override fun testMethodNameFor(executableId: ExecutableId, customName: String?): String { + val executableName = createExecutableName(executableId) + + val name = if (customName != null && customName !in existingMethodNames) { + customName + } else { + val base = customName ?: "test_${executableName.toSnakeCase()}" + nextIndexedMethodName(base) + } + existingMethodNames += name + return name + } + + override fun errorMethodNameFor(executableId: ExecutableId): String { + val executableName = createExecutableName(executableId) + val newName = when (val base = "test_${executableName.toSnakeCase()}_errors") { + !in existingMethodNames -> base + else -> nextIndexedMethodName(base) + } + existingMethodNames += newName + return newName + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt new file mode 100644 index 0000000000..f021f2f57c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt @@ -0,0 +1,51 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.tree.CgIncompleteMethodCall +import org.utbot.framework.codegen.model.constructor.util.importIfNeeded +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.PythonMethodId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.framework.plugin.api.util.exceptions +import org.utbot.python.framework.codegen.model.constructor.util.importIfNeeded + +class PythonCgCallableAccessManagerImpl(val context: CgContext) : CgCallableAccessManager, + CgContextOwner by context { + + override fun CgExpression?.get(methodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(methodId, this) + + override fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = + CgIncompleteMethodCall(staticMethodId, CgThisInstance(pythonAnyClassId)) + + override fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { + val resolvedArgs = args.resolve() + val constructorCall = CgConstructorCall(this, resolvedArgs) + newConstructorCall(this) + return constructorCall + } + + override fun CgIncompleteMethodCall.invoke(vararg args: Any?): CgMethodCall { + val resolvedArgs = args.resolve() + val methodCall = CgMethodCall(caller, method, resolvedArgs) + if (method is PythonMethodId) + newMethodCall(method) + return methodCall + } + + private fun newMethodCall(methodId: MethodId) { + importIfNeeded(methodId as PythonMethodId) + } + + private fun newConstructorCall(constructorId: ConstructorId) { + importIfNeeded(constructorId.classId) + for (exception in constructorId.exceptions) { + addExceptionIfNeeded(exception) + } + } + +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt new file mode 100644 index 0000000000..8f6b6ffab0 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -0,0 +1,343 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.fields.StateModificationInfo +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.* +import org.utbot.python.framework.codegen.model.tree.* + +class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(context) { + override fun assertEquality(expected: CgValue, actual: CgVariable) { + pythonDeepEquals(expected, actual) + } + + private fun generatePythonTestComments(execution: UtExecution) { + when (execution.result) { + is UtExplicitlyThrownException -> + (execution.result as UtExplicitlyThrownException).exception.message?.let { + emptyLineIfNeeded() + comment("raises $it") + } + else -> { + // nothing + } + } + } + + override fun createTestMethod(executableId: ExecutableId, execution: UtExecution): CgTestMethod = + withTestMethodScope(execution) { + val testMethodName = nameGenerator.testMethodNameFor(executableId, execution.testMethodName) + // TODO: remove this line when SAT-1273 is completed + execution.displayName = execution.displayName?.let { "${executableId.name}: $it" } + testMethod(testMethodName, execution.displayName) { + val statics = currentExecution!!.stateBefore.statics + rememberInitialStaticFields(statics) + context.codeGenLanguage.memoryObjects.clear() + +// val stateAnalyzer = ExecutionStateAnalyzer(execution) + val modificationInfo = StateModificationInfo() // stateAnalyzer.findModifiedFields() + // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states + val mainBody = { + substituteStaticFields(statics) + setupInstrumentation() + // build this instance + thisInstance = execution.stateBefore.thisInstance?.let { + variableConstructor.getOrCreateVariable(it) + } + // build arguments + for ((index, param) in execution.stateBefore.parameters.withIndex()) { + val name = paramNames[executableId]?.get(index) + methodArguments += variableConstructor.getOrCreateVariable(param, name) + } +// if (executableId is PythonMethodId) { +// existingVariableNames += executableId.name +// executableId.moduleName.split('.').forEach { +// existingVariableNames += it +// } +// } + rememberInitialEnvironmentState(modificationInfo) + recordActualResult() + generateResultAssertions() + rememberFinalEnvironmentState(modificationInfo) + generateFieldStateAssertions() + if (executableId is PythonMethodId) + generatePythonTestComments(execution) + } + + if (statics.isNotEmpty()) { + +tryBlock { + mainBody() + }.finally { + recoverStaticFields() + } + } else { + mainBody() + } + } + } + + private fun pythonBuildObject(objectNode: PythonTree.PythonTreeNode): CgValue { + return when (objectNode) { + is PythonTree.PrimitiveNode -> { + CgLiteral(objectNode.type, objectNode.repr) + } + is PythonTree.ListNode -> { + CgPythonList( + objectNode.items.map { pythonBuildObject(it) } + ) + } + is PythonTree.TupleNode -> { + CgPythonTuple( + objectNode.items.map { pythonBuildObject(it) } + ) + } + is PythonTree.SetNode -> { + CgPythonSet( + objectNode.items.map { pythonBuildObject(it) }.toSet() + ) + } + is PythonTree.DictNode -> { + CgPythonDict( + objectNode.items.map { (key, value) -> + pythonBuildObject(key) to pythonBuildObject(value) + }.toMap() + ) + } + is PythonTree.ReduceNode -> { + val id = objectNode.id + if (context.codeGenLanguage.memoryObjects.containsKey(id)) { + return context.codeGenLanguage.memoryObjects[id]!! + } + + val initArgs = objectNode.args.map { + pythonBuildObject(it) + } + val constructor = ConstructorId( + objectNode.constructor, + initArgs.map { it.type } + ) + + val obj = newVar(objectNode.type) { + CgConstructorCall( + constructor, + initArgs + ) + } + context.codeGenLanguage.memoryObjects[id] = obj + + val state = objectNode.state.map { (key, value) -> + key to pythonBuildObject(value) + }.toMap() + val listitems = objectNode.listitems.map { + pythonBuildObject(it) + } + val dictitems = objectNode.dictitems.map { (key, value) -> + pythonBuildObject(key) to pythonBuildObject(value) + } + + state.forEach { (key, value) -> + val fieldAccess = CgFieldAccess(obj, FieldId(objectNode.type, key)) + fieldAccess `=` value + } + listitems.forEach { + +CgMethodCall( + obj, + PythonMethodId( + obj.type as PythonClassId, + "append", + NormalizedPythonAnnotation(pythonNoneClassId.name), + listOf(RawPythonAnnotation(it.type.name)) + ), + listOf(it) + ) + } + dictitems.forEach { (key, value) -> + val index = CgPythonIndex( + value.type as PythonClassId, + obj, + key + ) + index `=` value + } + + return obj + } + else -> { + throw UnsupportedOperationException() + } + } + } + + private fun pythonDeepEquals(expected: CgValue, actual: CgVariable) { + require(expected is CgPythonTree) { + "Expected value have to be CgPythonTree but `${expected::class}` found" + } + val expectedValue = pythonBuildObject(expected.tree) + pythonDeepTreeEquals(expected.tree, expectedValue, actual) + } + + private fun pythonLenAssertConstructor(expected: CgVariable, actual: CgVariable): CgVariable { + val expectedValue = newVar(pythonIntClassId, "expected_length") { + CgGetLength(expected) + } + val actualValue = newVar(pythonIntClassId, "actual_length") { + CgGetLength(actual) + } + emptyLineIfNeeded() + testFrameworkManager.assertEquals(expectedValue, actualValue) + return expectedValue + } + + private fun pythonAssertElementsByKey( + expectedNode: PythonTree.PythonTreeNode, + expected: CgVariable, + actual: CgVariable, + iterator: CgReferenceExpression, + keyName: String = "index", + ) { + val elements = when (expectedNode) { + is PythonTree.ListNode -> expectedNode.items + is PythonTree.TupleNode -> expectedNode.items + is PythonTree.DictNode -> expectedNode.items.values + else -> throw UnsupportedOperationException() + } + if (elements.isNotEmpty()) { + val elementsHaveSameStructure = PythonTree.allElementsHaveSameStructure(elements) + val firstChild = + elements.first() // TODO: We can use only structure => we should use another element if the first is empty + + emptyLine() + if (elementsHaveSameStructure) { + val index = newVar(pythonNoneClassId, keyName) { + CgLiteral(pythonNoneClassId, "None") + } + forEachLoop { + innerBlock { + condition = index + iterable = iterator + val indexExpected = newVar(firstChild.type, "expected_element") { + CgPythonIndex( + pythonIntClassId, + expected, + index + ) + } + val indexActual = newVar(firstChild.type, "actual_element") { + CgPythonIndex( + pythonIntClassId, + actual, + index + ) + } + pythonDeepTreeEquals(firstChild, indexExpected, indexActual) + statements = currentBlock + } + } + } + else { + emptyLineIfNeeded() + testFrameworkManager.assertIsinstance(listOf(expected.type), actual) + } + } + } + + private fun pythonAssertBuiltinsCollection( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable, + expectedName: String, + elementName: String = "index", + ) { + val expectedCollection = newVar(expected.type, expectedName) { expected } + + val length = pythonLenAssertConstructor(expectedCollection, actual) + + val iterator = if (expectedNode is PythonTree.DictNode) expected else CgPythonRange(length) + pythonAssertElementsByKey(expectedNode, expectedCollection, actual, iterator, elementName) + } + + private fun pythonDeepTreeEquals( + expectedNode: PythonTree.PythonTreeNode, + expected: CgValue, + actual: CgVariable + ) { + if (expectedNode.comparable) { + emptyLineIfNeeded() + testFrameworkManager.assertEquals( + expected, + actual, + ) + return + } + when (expectedNode) { + is PythonTree.PrimitiveNode -> { + emptyLineIfNeeded() + testFrameworkManager.assertIsinstance( + listOf(expected.type), actual + ) + } + is PythonTree.ListNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_list" + ) + } + is PythonTree.TupleNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_tuple" + ) + } + is PythonTree.SetNode -> { + emptyLineIfNeeded() + testFrameworkManager.assertEquals( + expected, actual + ) + } + is PythonTree.DictNode -> { + pythonAssertBuiltinsCollection( + expectedNode, + expected, + actual, + "expected_dict", + "key" + ) + } + is PythonTree.ReduceNode -> { + if (expectedNode.state.isNotEmpty()) { + expectedNode.state.forEach { (field, value) -> + val fieldActual = newVar(value.type, "actual_$field") { + CgFieldAccess( + actual, FieldId( + value.type, + field + ) + ) + } + val fieldExpected = newVar(value.type, "expected_$field") { + CgFieldAccess( + expected, FieldId( + value.type, + field + ) + ) + } + pythonDeepTreeEquals(value, fieldExpected, fieldActual) + } + } else { + emptyLineIfNeeded() + testFrameworkManager.assertIsinstance( + listOf(expected.type), actual + ) + } + } + else -> {} + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt new file mode 100644 index 0000000000..ec417c5e03 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt @@ -0,0 +1,237 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import fj.data.Either +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager +import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor +import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.buildExceptionHandler +import org.utbot.framework.codegen.model.util.isAccessibleFrom +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.util.* +import org.utbot.python.framework.codegen.model.constructor.util.* +import java.util.* + +class PythonCgStatementConstructorImpl(context: CgContext) : + CgStatementConstructor, + CgContextOwner by context, + CgCallableAccessManager by CgComponents.getCallableAccessManagerBy(context) { + + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + + override fun newVar( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutable: Boolean, + init: () -> CgExpression + ): CgVariable { + val declarationOrVar: Either = + createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType, + model, + baseName, + isMock, + isMutable, + init + ) + + return declarationOrVar.either( + { declaration -> + currentBlock += declaration + + declaration.variable + }, + { variable -> variable } + ) + } + + override fun createDeclarationForNewVarAndUpdateVariableScopeOrGetExistingVariable( + baseType: ClassId, + model: UtModel?, + baseName: String?, + isMock: Boolean, + isMutableVar: Boolean, + init: () -> CgExpression + ): Either { + val baseExpr = init() + + val name = nameGenerator.variableName(baseType, baseName) + val (type, expr) = guardExpression(baseType, baseExpr) + + val declaration = buildDeclaration { + variableType = type + variableName = name + initializer = expr + } + updateVariableScope(declaration.variable, model) + return Either.left(declaration) + } + + override fun CgExpression.`=`(value: Any?) { + currentBlock += buildAssignment { + lValue = this@`=` + rValue = value.resolve() + } + } + + override fun CgExpression.and(other: CgExpression): CgLogicalAnd = + CgLogicalAnd(this, other) + + override fun CgExpression.or(other: CgExpression): CgLogicalOr = + CgLogicalOr(this, other) + + override fun ifStatement( + condition: CgExpression, + trueBranch: () -> Unit, + falseBranch: (() -> Unit)? + ): CgIfStatement { + val trueBranchBlock = block(trueBranch) + val falseBranchBlock = falseBranch?.let { block(it) } + return CgIfStatement(condition, trueBranchBlock, falseBranchBlock).also { + currentBlock += it + } + } + + override fun forLoop(init: CgForLoopBuilder.() -> Unit) { + currentBlock += buildForLoop(init) + } + + override fun whileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun doWhileLoop(condition: CgExpression, statements: () -> Unit) { + currentBlock += buildDoWhileLoop { + this.condition = condition + this.statements += block(statements) + } + } + + override fun forEachLoop(init: CgForEachLoopBuilder.() -> Unit) = withNameScope { + currentBlock += buildCgForEachLoop(init) + } + + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) + + override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = + buildTryCatch { + statements = block(init) + this.resources = resources + } + + override fun CgTryCatch.catch(exception: ClassId, init: (CgVariable) -> Unit): CgTryCatch { + val newHandler = buildExceptionHandler { + val e = declareVariable(exception, nameGenerator.variableName(exception.simpleName.replaceFirstChar { + it.lowercase( + Locale.getDefault() + ) + })) + this.exception = e + this.statements = block { init(e) } + } + return this.copy(handlers = handlers + newHandler) + } + + override fun CgTryCatch.finally(init: () -> Unit): CgTryCatch { + val finallyBlock = block(init) + return this.copy(finally = finallyBlock) + } + + override fun CgExpression.isInstance(value: CgExpression): CgIsInstance = TODO("Not yet implemented") + + override fun innerBlock(init: () -> Unit): CgInnerBlock = + CgInnerBlock(block(init)).also { + currentBlock += it + } + + override fun comment(text: String): CgComment = + CgSingleLineComment(text).also { + currentBlock += it + } + + override fun comment(): CgComment = + CgSingleLineComment("").also { + currentBlock += it + } + + override fun multilineComment(lines: List): CgComment = + CgMultilineComment(lines).also { + currentBlock += it + } + + override fun lambda(type: ClassId, vararg parameters: CgVariable, body: () -> Unit): CgAnonymousFunction { + return withNameScope { + for (parameter in parameters) { + declareParameter(parameter.type, parameter.name) + } + val paramDeclarations = parameters.map { CgParameterDeclaration(it) } + CgAnonymousFunction(type, paramDeclarations, block(body)) + } + } + + override fun annotation(classId: ClassId, argument: Any?): CgAnnotation { + val annotation = CgSingleArgAnnotation(classId, argument.resolve()) + addAnnotation(annotation) + return annotation + } + + override fun annotation(classId: ClassId, namedArguments: List>): CgAnnotation { + val annotation = CgMultipleArgsAnnotation( + classId, + namedArguments.mapTo(mutableListOf()) { (name, value) -> CgNamedAnnotationArgument(name, value) } + ) + addAnnotation(annotation) + return annotation + } + + override fun annotation( + classId: ClassId, + buildArguments: MutableList>.() -> Unit + ): CgAnnotation { + val arguments = mutableListOf>() + .apply(buildArguments) + .map { (name, value) -> CgNamedAnnotationArgument(name, value) } + val annotation = CgMultipleArgsAnnotation(classId, arguments.toMutableList()) + addAnnotation(annotation) + return annotation + } + + override fun returnStatement(expression: () -> CgExpression) { + currentBlock += CgReturnStatement(expression()) + } + + override fun throwStatement(exception: () -> CgExpression): CgThrowStatement = + CgThrowStatement(exception()).also { currentBlock += it } + + override fun emptyLine() { + currentBlock += CgEmptyLine() + } + + override fun emptyLineIfNeeded() { + val lastStatement = currentBlock.lastOrNull() ?: return + if (lastStatement is CgEmptyLine) return + emptyLine() + } + + override fun declareVariable(type: ClassId, name: String): CgVariable = + CgVariable(name, type).also { + updateVariableScope(it) + } + + override fun guardExpression(baseType: ClassId, expression: CgExpression): ExpressionWithType { + return ExpressionWithType(baseType, expression) + } + + override fun wrapTypeIfRequired(baseType: ClassId): ClassId = + if (baseType.isAccessibleFrom(testClassPackageName)) baseType else objectClassId +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt new file mode 100644 index 0000000000..6063fb8a88 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt @@ -0,0 +1,113 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.ParametrizedTestSource +import org.utbot.framework.codegen.model.constructor.CgMethodTestSet +import org.utbot.framework.codegen.model.constructor.TestClassModel +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor +import org.utbot.framework.codegen.model.tree.* + +internal class PythonCgTestClassConstructor(context: CgContext) : CgTestClassConstructor(context) { + override fun construct(testClassModel: TestClassModel): CgTestClassFile { + return buildTestClassFile { + this.testClass = withTestClassScope { constructTestClass(testClassModel) } + imports.addAll(context.collectedImports) + testsGenerationReport = this@PythonCgTestClassConstructor.testsGenerationReport + } + } + + override fun constructTestClass(testClassModel: TestClassModel): CgTestClass { + return buildTestClass { + id = currentTestClass + + if (currentTestClass != outerMostTestClass) { + isNested = true + isStatic = testFramework.nestedClassesShouldBeStatic + testFrameworkManager.annotationForNestedClasses?.let { + currentTestClassContext.collectedTestClassAnnotations += it + } + } + if (testClassModel.nestedClasses.isNotEmpty()) { + testFrameworkManager.annotationForOuterClasses?.let { + currentTestClassContext.collectedTestClassAnnotations += it + } + } + + body = buildTestClassBody { + for (nestedClass in testClassModel.nestedClasses) { + nestedClassRegions += CgSimpleRegion( + "Tests for ${nestedClass.classUnderTest.simpleName}", + listOf( + withNestedClassScope(nestedClass) { constructTestClass(nestedClass) } + ) + ) + } + + for (testSet in testClassModel.methodTestSets) { + updateCurrentExecutable(testSet.executableId) + val currentMethodUnderTestRegions = constructTestSet(testSet) ?: continue + val executableUnderTestCluster = CgExecutableUnderTestCluster( + "Test suites for executable $currentExecutable", + currentMethodUnderTestRegions + ) + testMethodRegions += executableUnderTestCluster + } + + val additionalMethods = currentTestClassContext.cgDataProviderMethods + + dataProvidersAndUtilMethodsRegion += CgStaticsRegion( + "Data providers and utils methods", + additionalMethods + ) + } + // It is important that annotations, superclass and interfaces assignment is run after + // all methods are generated so that all necessary info is already present in the context + with (currentTestClassContext) { + annotations += collectedTestClassAnnotations + superclass = testFramework.testSuperClass + interfaces += collectedTestClassInterfaces + } + } + } + + override fun constructTestSet(testSet: CgMethodTestSet): List>? { + if (testSet.executions.isEmpty()) { + return null + } + + allExecutions = testSet.executions + + val (methodUnderTest, _, _, clustersInfo) = testSet + val regions = mutableListOf>() + + when (context.parametrizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> { + for ((clusterSummary, executionIndices) in clustersInfo) { + val currentTestCaseTestMethods = mutableListOf() + emptyLineIfNeeded() + for (i in executionIndices) { + runCatching { + currentTestCaseTestMethods += methodConstructor.createTestMethod(methodUnderTest, testSet.executions[i]) + }.onFailure { e -> processFailure(testSet, e) } + } + val clusterHeader = clusterSummary?.header + val clusterContent = clusterSummary?.content + ?.split('\n') + ?.let { CgTripleSlashMultilineComment(it) } + regions += CgTestMethodCluster(clusterHeader, clusterContent, currentTestCaseTestMethods) + + testsGenerationReport.addTestsByType(testSet, currentTestCaseTestMethods) + } + } + ParametrizedTestSource.PARAMETRIZE -> {} + } + + val errors = testSet.allErrors + if (errors.isNotEmpty()) { + regions += methodConstructor.errorMethod(testSet.executableId, errors) + testsGenerationReport.addMethodErrors(testSet, errors) + } + + return regions + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt new file mode 100644 index 0000000000..d2f3f875a9 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt @@ -0,0 +1,39 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor +import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.* +import org.utbot.python.framework.codegen.model.tree.* + +class PythonCgVariableConstructor(context_: CgContext) : CgVariableConstructor(context_) { + private val nameGenerator = CgComponents.getNameGeneratorBy(context) + + override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { + val baseName = name ?: nameGenerator.nameFrom(model.classId) + return valueByModel.getOrPut(model) { + when (model) { + is PythonBoolModel -> CgLiteral(model.classId, model.value) + is PythonPrimitiveModel -> CgLiteral(model.classId, model.value) + is PythonTreeModel -> CgPythonTree(model.classId, model.tree) + is PythonInitObjectModel -> constructInitObjectModel(model, baseName) + is PythonDictModel -> CgPythonDict(model.stores.map { getOrCreateVariable(it.key) to getOrCreateVariable(it.value) }.toMap()) + is PythonListModel -> CgPythonList(model.stores.map { getOrCreateVariable(it) }) + is PythonSetModel -> CgPythonSet(model.stores.map { getOrCreateVariable(it) }.toSet()) + is PythonTupleModel -> CgPythonTuple(model.stores.map { getOrCreateVariable(it) }) + is PythonDefaultModel -> CgPythonRepr(model.classId, model.repr) + is PythonModel -> error("Unexpected PythonModel: ${model::class}") + else -> super.getOrCreateVariable(model, name) + } + } + } + + private fun constructInitObjectModel(model: PythonInitObjectModel, baseName: String): CgVariable { + return newVar(model.classId, baseName) { CgConstructorCall( + ConstructorId(model.classId, model.initValues.map { it.classId }), + model.initValues.map { getOrCreateVariable(it) } + ) } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt new file mode 100644 index 0000000000..7385b4d6e1 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonTestFrameworkManager.kt @@ -0,0 +1,140 @@ +package org.utbot.python.framework.codegen.model.constructor.tree + +import org.utbot.framework.codegen.model.constructor.TestClassContext +import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.framework.plugin.api.python.pythonBoolClassId +import org.utbot.python.framework.codegen.model.Pytest +import org.utbot.python.framework.codegen.model.Unittest +import org.utbot.python.framework.codegen.model.tree.CgPythonAssertEquals +import org.utbot.python.framework.codegen.model.tree.CgPythonFunctionCall +import org.utbot.python.framework.codegen.model.tree.CgPythonTuple + +internal class PytestManager(context: CgContext) : TestFrameworkManager(context) { + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Pytest) { "According to settings, Pytest was expected, but got: $testFramework" } + block() + } + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + } + + override val dataProviderMethodsHolder: TestClassContext get() = TODO() + override val annotationForNestedClasses: CgAnnotation? + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation? + get() = TODO("Not yet implemented") + + override fun assertEquals(expected: CgValue, actual: CgValue) { + +CgPythonAssertEquals( + CgEqualTo(actual, expected) + ) + } + + override fun assertIsinstance(types: List, actual: CgVariable) { + +CgPythonAssertEquals( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].name) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + ), + ), + ) + } +} + +internal class UnittestManager(context: CgContext) : TestFrameworkManager(context) { + override val dataProviderMethodsHolder: TestClassContext + get() = TODO() + override val annotationForNestedClasses: CgAnnotation? + get() = TODO("Not yet implemented") + override val annotationForOuterClasses: CgAnnotation? + get() = TODO("Not yet implemented") + + override fun expectException(exception: ClassId, block: () -> Unit) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + block() + } + + override fun createDataProviderAnnotations(dataProviderMethodName: String): MutableList { + TODO("Not yet implemented") + } + + override fun createArgList(length: Int): CgVariable { + TODO("Not yet implemented") + } + + override fun collectParameterizedTestAnnotations(dataProviderMethodName: String?): Set { + TODO("Not yet implemented") + } + + override fun passArgumentsToArgsVariable(argsVariable: CgVariable, argsArray: CgVariable, executionIndex: Int) { + TODO("Not yet implemented") + } + + override fun addTestDescription(description: String) = Unit + + override fun disableTestMethod(reason: String) { + require(testFramework is Unittest) { "According to settings, Unittest was expected, but got: $testFramework" } + + collectedMethodAnnotations += CgMultipleArgsAnnotation( + skipAnnotationClassId, + mutableListOf( + CgNamedAnnotationArgument( + name = "value", + value = reason.resolve() + ) + ) + ) + } + + private val skipAnnotationClassId = BuiltinClassId( + name = "skip", + canonicalName = "unittest.skip", + simpleName = "skip" + ) + + override fun assertIsinstance(types: List, actual: CgVariable) { + +assertions[assertTrue]( + CgPythonFunctionCall( + pythonBoolClassId, + "isinstance", + listOf( + actual, + if (types.size == 1) + CgLiteral(pythonAnyClassId, types[0].name) + else + CgPythonTuple(types.map { CgLiteral(pythonAnyClassId, it.name) }) + ), + ), + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt new file mode 100644 index 0000000000..378a437670 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/util/ConstructorUtils.kt @@ -0,0 +1,29 @@ +package org.utbot.python.framework.codegen.model.constructor.util + +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.PersistentSet +import org.utbot.framework.codegen.PythonUserImport +import org.utbot.framework.codegen.model.constructor.context.CgContextOwner +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.PythonMethodId + +internal fun CgContextOwner.importIfNeeded(method: PythonMethodId) { + collectedImports += PythonUserImport(method.moduleName) +} + +internal fun CgContextOwner.importIfNeeded(pyClass: PythonClassId) { + collectedImports += PythonUserImport(pyClass.moduleName) +} + +internal operator fun PersistentList.plus(element: T): PersistentList = + this.add(element) + +internal operator fun PersistentList.plus(other: PersistentList): PersistentList = + this.addAll(other) + +internal operator fun PersistentSet.plus(element: T): PersistentSet = + this.add(element) + +internal operator fun PersistentSet.plus(other: PersistentSet): PersistentSet = + this.addAll(other) + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt new file mode 100644 index 0000000000..f8dbe9f449 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -0,0 +1,468 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.apache.commons.text.StringEscapeUtils +import org.utbot.common.WorkaroundReason +import org.utbot.common.workaround +import org.utbot.framework.codegen.* +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.util.CgPrinter +import org.utbot.framework.codegen.model.util.CgPrinterImpl +import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.framework.plugin.api.python.pythonBuiltinsModuleName +import org.utbot.python.framework.codegen.model.tree.* + +internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer), CgPythonVisitor { + override val regionStart: String = "# region" + override val regionEnd: String = "# endregion" + + override val statementEnding: String = "" + + override val logicalAnd: String + get() = "and" + + override val logicalOr: String + get() = "or" + + override val language: CodegenLanguage = CodegenLanguage.PYTHON + + override val langPackage: String = "python" + + override fun visit(element: CgTestClassFile) { + renderClassFileImports(element) + + println() + println() + + element.testClass.accept(this) + } + + override fun visit(element: AbstractCgClass<*>) { + TODO("Not yet implemented") + } + + override fun visit(element: CgCommentedAnnotation) { + print("#") + element.annotation.accept(this) + } + + override fun visit(element: CgSingleArgAnnotation) { + print("") + } + + override fun visit(element: CgMultipleArgsAnnotation) { + print("") + } + + override fun visit(element: CgSingleLineComment) { + println("# ${element.comment}") + } + + override fun visit(element: CgAbstractMultilineComment) { + visit(element as CgElement) + } + + override fun visit(element: CgTripleSlashMultilineComment) { + element.lines.forEach { line -> + println("# $line") + } + } + + override fun visit(element: CgMultilineComment) { + val lines = element.lines + if (lines.isEmpty()) return + + if (lines.size == 1) { + print("# ${lines.first()}") + return + } + + // print lines saving indentation + print("\"\"\"") + println(lines.first()) + lines.subList(1, lines.lastIndex).forEach { println(it) } + print(lines.last()) + println("\"\"\"") + } + + override fun visit(element: CgDocumentationComment) { + if (element.lines.all { it.isEmpty() }) return + + println("\"\"\"") + for (line in element.lines) line.accept(this) + println("\"\"\"") + } + + override fun visit(element: CgErrorWrapper) { + element.expression.accept(this) + } + + override fun visit(element: CgTestClass) { + print("class ") + print(element.simpleName) + if (element.superclass != null) { + print("(${element.superclass!!.asString()})") + } + println(":") + withIndent { element.body.accept(this) } + println("") + } + + override fun visit(element: CgTestClassBody) { + TODO("Not yet implemented") + } + + override fun visit(element: CgTryCatch) { + println("try") + // TODO introduce CgBlock + visit(element.statements) + for ((exception, statements) in element.handlers) { + print("except") + renderExceptionCatchVariable(exception) + println("") + // TODO introduce CgBlock + visit(statements, printNextLine = element.finally == null) + } + element.finally?.let { + print("finally") + // TODO introduce CgBlock + visit(element.finally!!, printNextLine = true) + } + } + + override fun visit(element: CgArrayAnnotationArgument) { + TODO("Not yet implemented") + } + + override fun visit(element: CgAnonymousFunction) { + print("lambda ") + element.parameters.renderSeparated() + print(": ") + + visit(element.body) + } + + override fun visit(element: CgEqualTo) { + element.left.accept(this) + print(" == ") + element.right.accept(this) + } + + override fun visit(element: CgTypeCast) { + TODO("Not yet implemented") + } + + override fun visit(element: CgNotNullAssertion) { + TODO("Not yet implemented") + } + + override fun visit(element: CgAllocateArray) { + TODO("Not yet implemented") + } + + override fun visit(element: CgAllocateInitializedArray) { + TODO("Not yet implemented") + } + + override fun visit(element: CgArrayInitializer) { + TODO("Not yet implemented") + } + + override fun visit(element: CgSwitchCaseLabel) { + TODO("Not yet implemented") + } + + override fun visit(element: CgSwitchCase) { + TODO("Not yet implemented") + } + + override fun visit(element: CgParameterDeclaration) { + print(element.name.escapeNamePossibleKeyword()) + if (element.type.name != "") + print(": ") + print(element.type.name) + } + + override fun visit(element: CgGetLength) { + print("len(") + element.variable.accept(this) + print(")") + } + + override fun visit(element: CgGetJavaClass) { + TODO("Not yet implemented") + } + + override fun visit(element: CgGetKotlinClass) { + TODO("Not yet implemented") + } + + override fun visit(element: CgConstructorCall) { + print(element.executableId.classId.name) + renderExecutableCallArguments(element) + } + + override fun renderRegularImport(regularImport: RegularImport) { + val escapedImport = getEscapedImportRendering(regularImport) + println("import $escapedImport") + } + + override fun renderStaticImport(staticImport: StaticImport) { + TODO("Not yet implemented") + } + + override fun renderClassFileImports(element: CgTestClassFile) { + element.imports + .toSet() + .filterIsInstance() + .sortedBy { it.order } + .forEach { renderPythonImport(it) } + } + + private fun renderPythonImport(pythonImport: PythonImport) { + if (pythonImport is PythonSysPathImport) { + println("sys.path.append('${pythonImport.sysPath}')") + } + else if (pythonImport.moduleName == null) { + println("import ${pythonImport.importName}") + } + else { + println("from ${pythonImport.moduleName} import ${pythonImport.importName}") + } + } + + override fun renderMethodSignature(element: CgTestMethod) { + print("def ") + print(element.name) + + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + val selfParameter = CgThisInstance(pythonAnyClassId) + (listOf(selfParameter) + element.parameters).renderSeparated(newLinesNeeded) + print(")") + } + + override fun renderMethodSignature(element: CgErrorTestMethod) { + print("def ") + print(element.name) + print("(") + val selfParameter = CgThisInstance(pythonAnyClassId) + listOf(selfParameter).renderSeparated() + print(")") + } + + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { + TODO("Not yet implemented") + } + + override fun visit(element: CgInnerBlock) { + withIndent { + for (statement in element.statements) { + statement.accept(this) + } + } + } + + override fun renderForLoopVarControl(element: CgForLoop) { + print("for ") + visit(element.condition) + print(" in ") + element.initialization.accept(this@CgPythonRenderer) + println(":") + } + + override fun renderDeclarationLeftPart(element: CgDeclaration) { + visit(element.variable) + } + + override fun toStringConstantImpl(byte: Byte): String { + return "b'$byte'" + } + + override fun toStringConstantImpl(short: Short): String { + return "$short" + } + + override fun toStringConstantImpl(int: Int): String { + return "$int" + } + + override fun toStringConstantImpl(long: Long): String { + return "$long" + } + + override fun toStringConstantImpl(float: Float): String { + return "$float" + } + + override fun renderAccess(caller: CgExpression) { + print(".") + } + + override fun renderTypeParameters(typeParameters: TypeParameters) { + if (typeParameters.parameters.isNotEmpty()) { + print("[") + if (typeParameters is WildcardTypeParameter) { + print("typing.Any") + } else { + print(typeParameters.parameters.joinToString { it.name }) + } + print("]") + } + } + + override fun renderExecutableCallArguments(executableCall: CgExecutableCall) { + print("(") + executableCall.arguments.renderSeparated() + print(")") + } + + override fun renderExceptionCatchVariable(exception: CgVariable) { + print(exception.name.escapeNamePossibleKeyword()) + } + + override fun escapeNamePossibleKeywordImpl(s: String): String = s + override fun renderClassVisibility(classId: ClassId) { + TODO("Not yet implemented") + } + + override fun renderClassModality(aClass: AbstractCgClass<*>) { + TODO("Not yet implemented") + } + + override fun visit(block: List, printNextLine: Boolean) { + println(":") + + val isBlockTooLarge = workaround(WorkaroundReason.LONG_CODE_FRAGMENTS) { block.size > 120 } + + withIndent { + if (isBlockTooLarge) { + print("\"\"\"") + println(" This block of code is ${block.size} lines long and could lead to compilation error") + } + + for (statement in block) { + statement.accept(this) + } + + if (isBlockTooLarge) println("\"\"\"") + } + + if (printNextLine) println() + } + + override fun visit(element: CgThisInstance) { + print("self") + } + + override fun visit(element: CgMethod) { + visit(element.statements, printNextLine = false) + } + + override fun visit(element: CgMethodCall) { + if (element.caller == null) { + val module = (element.executableId.classId as PythonClassId).moduleName + if (module != pythonBuiltinsModuleName) { + print("$module.") + } + } else { + element.caller!!.accept(this) + print(".") + } + print(element.executableId.name) + + renderTypeParameters(element.typeParameters) + renderExecutableCallArguments(element) + } + + override fun visit(element: CgPythonRepr) { + print(element.content) + } + + override fun visit(element: CgPythonIndex) { + visit(element.obj) + print("[") + element.index.accept(this) + print("]") + } + + override fun visit(element: CgPythonFunctionCall) { + print(element.name) + print("(") + val newLinesNeeded = element.parameters.size > maxParametersAmountInOneLine + element.parameters.renderSeparated(newLinesNeeded) + print(")") + } + + override fun visit(element: CgPythonAssertEquals) { + print("${element.keyword} ") + element.expression.accept(this) + println() + } + + override fun visit(element: CgPythonRange) { + print("range(") + listOf(element.start, element.stop, element.step).renderSeparated() + print(")") + } + + override fun visit(element: CgPythonList) { + print("[") + element.elements.renderSeparated() + print("]") + } + + override fun visit(element: CgPythonTuple) { + print("(") + element.elements.renderSeparated() + if (element.elements.size == 1) { + print(",") + } + print(")") + } + + override fun visit(element: CgPythonSet) { + if (element.elements.isEmpty()) + print("set()") + else { + print("{") + element.elements.toList().renderSeparated() + print("}") + } + } + + override fun visit(element: CgPythonDict) { + print("{") + element.elements.map { (key, value) -> + key.accept(this) + print(": ") + value.accept(this) + print(", ") + } + print("}") + } + + override fun visit(element: CgForEachLoop) { + print("for ") + element.condition.accept(this) + print(" in ") + element.iterable.accept(this) + println(":") + withIndent { element.statements.forEach { it.accept(this) } } + } + + override fun visit(element: CgLiteral) { + print(element.value.toString()) + } + + override fun String.escapeCharacters(): String = + StringEscapeUtils + .escapeJava(this) + .replace("'", "\\'") + .replace("\\f", "\\u000C") + .replace("\\xxx", "\\\u0058\u0058\u0058") +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt new file mode 100644 index 0000000000..ffc02d4f96 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonVisitor.kt @@ -0,0 +1,18 @@ +package org.utbot.python.framework.codegen.model.constructor.visitor + +import org.utbot.framework.codegen.model.visitor.CgVisitor +import org.utbot.python.framework.codegen.model.tree.* + +interface CgPythonVisitor : CgVisitor { + + fun visit(element: CgPythonRepr): R + fun visit(element: CgPythonIndex): R + fun visit(element: CgPythonAssertEquals): R + fun visit(element: CgPythonFunctionCall): R + fun visit(element: CgPythonRange): R + fun visit(element: CgPythonDict): R + fun visit(element: CgPythonTuple): R + fun visit(element: CgPythonList): R + fun visit(element: CgPythonSet): R + +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt new file mode 100644 index 0000000000..bb48ae9faf --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/tree/CgPythonElement.kt @@ -0,0 +1,101 @@ +package org.utbot.python.framework.codegen.model.tree + +import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.codegen.model.visitor.CgVisitor +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.* +import org.utbot.python.framework.codegen.model.constructor.visitor.CgPythonVisitor + +interface CgPythonElement : CgElement { + override fun accept(visitor: CgVisitor): R = visitor.run { + if (visitor is CgPythonVisitor) { + when (val element = this@CgPythonElement) { + is CgPythonRepr -> visitor.visit(element) + is CgPythonIndex -> visitor.visit(element) + is CgPythonAssertEquals -> visitor.visit(element) + is CgPythonFunctionCall -> visitor.visit(element) + is CgPythonRange -> visitor.visit(element) + is CgPythonList -> visitor.visit(element) + is CgPythonSet -> visitor.visit(element) + is CgPythonDict -> visitor.visit(element) + is CgPythonTuple -> visitor.visit(element) + else -> throw IllegalArgumentException("Can not visit element of type ${element::class}") + } + } + else { + super.accept(visitor) + } + } +} + +class CgPythonTree( + override val type: ClassId, + val tree: PythonTree.PythonTreeNode +) : CgValue, CgPythonElement + +class CgPythonRepr( + override val type: ClassId, + val content: String +) : CgValue, CgPythonElement + +class CgPythonAssertEquals( + val expression: CgExpression, + val keyword: String = "assert", +) : CgStatement, CgPythonElement + +class CgPythonFunctionCall( + override val type: PythonClassId, + val name: String, + val parameters: List, +) : CgExpression, CgPythonElement + +class CgPythonIndex( + override val type: PythonClassId, + val obj: CgVariable, + val index: CgExpression, +): CgValue, CgPythonElement + +class CgPythonRange( + val start: CgValue, + val stop: CgValue, + val step: CgValue, +) : CgValue, CgPythonElement { + override val type: PythonClassId + get() = pythonRangeClassId + + constructor(stop: Int): this( + CgLiteral(pythonIntClassId, 0), + CgLiteral(pythonIntClassId, stop), + CgLiteral(pythonIntClassId, 1), + ) + + constructor(stop: CgValue): this( + CgLiteral(pythonIntClassId, 0), + stop, + CgLiteral(pythonIntClassId, 1), + ) +} + +class CgPythonList( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonListClassId +} + +class CgPythonTuple( + val elements: List +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonTupleClassId +} + +class CgPythonSet( + val elements: Set +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonSetClassId +} + +class CgPythonDict( + val elements: Map +) : CgValue, CgPythonElement { + override val type: PythonClassId = pythonDictClassId +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt new file mode 100644 index 0000000000..25246d03df --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/ConstantModelProvider.kt @@ -0,0 +1,44 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.PythonPrimitiveModel +import org.utbot.fuzzer.FuzzedContext +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider.Companion.yieldValue +import java.math.BigDecimal +import java.math.BigInteger + +class ConstantModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + description.concreteValues + .asSequence() + .flatMap { (classId, value, op) -> + val model = (classId as? PythonClassId)?.let { PythonPrimitiveModel(value, it) } + sequenceOf( + model?.fuzzed { summary = "%var% = $value" }, + modifyValue(model, op) + ) + } + .filterNotNull() + .forEach { value -> + description.parametersMap.getOrElse(value.model.classId) { emptyList() }.forEach { index -> + yieldValue(index, value) + } + } + } + + private fun modifyValue(model: PythonPrimitiveModel?, op: FuzzedContext): FuzzedValue? { + if (op !is FuzzedContext.Comparison || model == null) return null + val multiplier = if (op == FuzzedContext.Comparison.LT || op == FuzzedContext.Comparison.GE) -1 else 1 + return when (val value = model.value) { + is BigInteger -> value + multiplier.toBigInteger() + is BigDecimal -> value + multiplier.toBigDecimal() + else -> null + }?.let { PythonPrimitiveModel(it, model.classId as PythonClassId).fuzzed { summary = "%var% ${ + (if (op == FuzzedContext.Comparison.EQ || op == FuzzedContext.Comparison.LE || op == FuzzedContext.Comparison.GE) { + op.reverse() + } else op).sign + } ${model.value}" } } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt new file mode 100644 index 0000000000..a08b2a56a2 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/DefaultValuesModelProvider.kt @@ -0,0 +1,24 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.python.PythonDefaultModel +import org.utbot.fuzzer.FuzzedParameter +import org.utbot.python.typing.PythonTypesStorage + +class DefaultValuesModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + val generated = Array(description.parameters.size) { 0 } + description.parametersMap.forEach { (classId, parameterIndices) -> + val pythonClassIdInfo = PythonTypesStorage.findPythonClassIdInfoByName(classId.name) ?: return@forEach + pythonClassIdInfo.preprocessedInstances?.forEach { instance -> + parameterIndices.forEach { index -> + generated[index] += 1 + if (generated[index] < 10) + yield(FuzzedParameter( + index, + PythonDefaultModel(instance, pythonClassIdInfo.pythonClassId).fuzzed() + )) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt new file mode 100644 index 0000000000..bd06183c78 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/GeneralPythonModelProvider.kt @@ -0,0 +1,51 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.fuzzer.* + +val defaultPythonModelProvider = getDefaultPythonModelProvider(recursionDepth = 4) + +fun getDefaultPythonModelProvider(recursionDepth: Int): ModelProvider = + ModelProvider.of( + ConstantModelProvider(recursionDepth), + DefaultValuesModelProvider(recursionDepth), + GenericModelProvider(recursionDepth), + UnionModelProvider(recursionDepth), + OptionalModelProvider(recursionDepth), + InitModelProvider(recursionDepth) + ) + +abstract class PythonModelProvider(protected val recursionDepth: Int): ModelProvider { + override fun generate(description: FuzzedMethodDescription): Sequence = + generate( + PythonFuzzedMethodDescription( + description.name, + description.returnType, + description.parameters.map { (it as? NormalizedPythonAnnotation) ?: pythonAnyClassId }, + description.concreteValues + ) + ) + + abstract fun generate(description: PythonFuzzedMethodDescription): Sequence +} + +class PythonFuzzedMethodDescription( + name: String, + returnType: ClassId, + parameters: List, + concreteValues: Collection = emptyList() +): FuzzedMethodDescription(name, returnType, parameters, concreteValues) + +fun substituteTypesByIndex( + description: PythonFuzzedMethodDescription, + newTypes: List +): PythonFuzzedMethodDescription { + return PythonFuzzedMethodDescription( + description.name, + description.returnType, + newTypes, + description.concreteValues + ) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt new file mode 100644 index 0000000000..237c670c67 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/GenericModelProvider.kt @@ -0,0 +1,103 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.framework.plugin.api.python.* +import org.utbot.fuzzer.* +import org.utbot.python.typing.DictAnnotation +import org.utbot.python.typing.ListAnnotation +import org.utbot.python.typing.SetAnnotation +import org.utbot.python.typing.parseGeneric +import java.lang.Integer.min +import kotlin.random.Random + +class GenericModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + private val maxGenNum = 10 + + override fun generate(description: PythonFuzzedMethodDescription): Sequence = sequence { + fun fuzzGeneric( + parameters: List, + index: Int, + modelConstructor: (List>) -> T? + ) = sequence inner@{ + if (recursionDepth <= 0) + return@inner + + val syntheticGenericType = FuzzedMethodDescription( + "${description.name}", + pythonNoneClassId, + parameters, + description.concreteValues + ) + + fuzz(syntheticGenericType, getDefaultPythonModelProvider(recursionDepth - 1)) + .randomChunked() + .mapNotNull(modelConstructor) + .forEach { + yield(FuzzedParameter(index, it.fuzzed())) + } + } + + fun genList(listAnnotation: ListAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(listAnnotation.elemAnnotation), index) { list -> + PythonListModel( + list.size, + list.flatten().mapNotNull { it.model as? PythonModel } + ) + } + } + + fun genDict(dictAnnotation: DictAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(dictAnnotation.keyAnnotation, dictAnnotation.valueAnnotation), index) { list -> + if (list.any { it.any { value -> value.model !is PythonModel } }) + return@fuzzGeneric null + PythonDictModel( + list.size, + list.associate { pair -> + (pair[0].model as PythonModel) to (pair[1].model as PythonModel) + } + ) + } + } + + fun genSet(setAnnotation: SetAnnotation, index: Int): Sequence { + return fuzzGeneric(listOf(setAnnotation.elemAnnotation), index) { list -> + PythonSetModel( + list.size, + list.flatten().mapNotNull { it.model as? PythonModel }.toSet(), + ) + } + } + + description.parametersMap.forEach { (classId, parameterIndices) -> + val parsedAnnotation = parseGeneric(classId as NormalizedPythonAnnotation) ?: return@forEach + parameterIndices.forEach { index -> + val generatedModels = when (parsedAnnotation) { + is ListAnnotation -> genList(parsedAnnotation, index) + is DictAnnotation -> genDict(parsedAnnotation, index) + is SetAnnotation -> genSet(parsedAnnotation, index) + } + yieldAll( + generatedModels.take(maxGenNum).distinctBy { fuzzedParameter -> + fuzzedParameter.value.model.toString() + } + ) + } + } + } +} + +const val MAX_CONTAINER_SIZE = 7 + +fun Sequence>.randomChunked(): Sequence>> { + val seq = this + val itemsToGenerateFrom = seq.take(MAX_CONTAINER_SIZE * 2).toList() + return sequenceOf(emptyList>()) + generateSequence { + if (itemsToGenerateFrom.isEmpty()) + return@generateSequence null + val size = Random.nextInt(1, min(MAX_CONTAINER_SIZE, itemsToGenerateFrom.size) + 1) + (0 until size).map { + val index = Random.nextInt(0, itemsToGenerateFrom.size) + itemsToGenerateFrom[index] + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt new file mode 100644 index 0000000000..625f2b21cf --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/InitModelProvider.kt @@ -0,0 +1,41 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.python.PythonInitObjectModel +import org.utbot.framework.plugin.api.python.PythonModel +import org.utbot.fuzzer.* +import org.utbot.python.typing.PythonTypesStorage + +class InitModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription) = sequence { + if (recursionDepth <= 0) + return@sequence + + description.parametersMap.forEach { (classId, parameterIndices) -> + val type = PythonTypesStorage.findPythonClassIdInfoByName(classId.name) ?: return@forEach + val initSignature = type.initSignature ?: return@forEach + + val models: Sequence = + if (initSignature.isEmpty()) + sequenceOf(PythonInitObjectModel(classId.name, emptyList())) + else { + val constructor = FuzzedMethodDescription( + type.pythonClassId.name, + classId, + initSignature, + description.concreteValues + ) + + val modelProvider = getDefaultPythonModelProvider(recursionDepth - 1) + fuzz(constructor, modelProvider).map { initValues -> + PythonInitObjectModel(classId.name, initValues.mapNotNull { it.model as? PythonModel }) + } + } + + parameterIndices.forEach { index -> + models.forEach { model -> + yield(FuzzedParameter(index, model.fuzzed())) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt new file mode 100644 index 0000000000..fc216724ec --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/OptionalModelProvider.kt @@ -0,0 +1,35 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.framework.plugin.api.python.pythonNoneClassId +import org.utbot.fuzzer.FuzzedParameter + +class OptionalModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription): Sequence { + var result = emptySequence() + description.parametersMap.forEach { (classId, parameterIndices) -> + val regex = Regex("typing.Optional\\[(.*)]") + val annotation = classId.name + val match = regex.matchEntire(annotation) ?: return@forEach + parameterIndices.forEach { index -> + val descriptionWithNoneType = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(pythonNoneClassId.name) else pythonAnyClassId + } + ) + val modelProvider = getDefaultPythonModelProvider(recursionDepth) + result += modelProvider.generate(descriptionWithNoneType) + val descriptionWithNonNoneType = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(match.groupValues[1]) else pythonAnyClassId + } + ) + result += modelProvider.generate(descriptionWithNonNoneType) + } + } + return result + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt b/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt new file mode 100644 index 0000000000..b7e44d183a --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/providers/UnionModelProvider.kt @@ -0,0 +1,28 @@ +package org.utbot.python.providers + +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.fuzzer.FuzzedParameter + +class UnionModelProvider(recursionDepth: Int): PythonModelProvider(recursionDepth) { + override fun generate(description: PythonFuzzedMethodDescription): Sequence { + var result = emptySequence() + description.parametersMap.forEach { (classId, parameterIndices) -> + val regex = Regex("typing.Union\\[(.*), *(.*)]") + val annotation = classId.name + val match = regex.matchEntire(annotation) ?: return@forEach + parameterIndices.forEach { index -> + for (newAnnotation in listOf(match.groupValues[1], match.groupValues[2])) { + val newDescription = substituteTypesByIndex( + description, + (0 until description.parameters.size).map { + if (it == index) NormalizedPythonAnnotation(newAnnotation) else pythonAnyClassId + } + ) + result += getDefaultPythonModelProvider(recursionDepth).generate(newDescription) + } + } + } + return result + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt new file mode 100644 index 0000000000..7c961886bd --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/FindAnnotations.kt @@ -0,0 +1,204 @@ +package org.utbot.python.typing + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import org.utbot.python.PythonMethod +import org.utbot.python.code.ArgInfoCollector +import org.utbot.python.utils.* + +private val logger = KotlinLogging.logger {} + +object AnnotationFinder { + + private const val MAX_CANDIDATES_FOR_PARAM = 100 + + fun findAnnotations( + argInfoCollector: ArgInfoCollector, + methodUnderTest: PythonMethod, + existingAnnotations: Map, + moduleToImport: String, + directoriesForSysPath: Set, + pythonPath: String, + isCancelled: () -> Boolean, + storageForMypyMessages: MutableList + ): Sequence> { + + logger.debug("Finding candidates...") + val annotationsToCheck = findTypeCandidates(argInfoCollector, existingAnnotations) + logger.debug("Found") + + return MypyAnnotations.getCheckedByMypyAnnotations( + methodUnderTest, + annotationsToCheck, + moduleToImport, + directoriesForSysPath, + pythonPath, + isCancelled, + storageForMypyMessages + ) + } + + private fun increaseValue( + map: MutableMap, + key: NormalizedPythonAnnotation, + by: Double + ) { + if (key == pythonAnyClassId) + return + map[key] = (map[key] ?: 0.0) + by + } + + private fun getInitCandidateMap(): MutableMap { + val candidates = mutableMapOf() // key: type, value: priority + PythonTypesStorage.builtinTypes.associateByTo( + destination = candidates, + { AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(PythonClassId("builtins.$it")) }, + { 0.0 } + ) + return candidates + } + + private const val EPS = 1e-6 + + private fun candidatesMapToRating(candidates: Map) = + candidates.toList().sortedByDescending { it.second }.map { it.first } + + private fun increaseForProjectClasses(candidates: MutableMap) { + candidates.keys.forEach { typeName -> + if (PythonTypesStorage.isClassFromProject(typeName)) + increaseValue(candidates, typeName, EPS) + } + } + + private fun increaseForGenerics(candidates: MutableMap) { + candidates.keys.forEach { typeName -> + if (isGeneric(typeName)) + increaseValue(candidates, typeName, EPS) + } + } + + private fun calcAdd(foundCandidates: Int): Double = + if (foundCandidates == 0) 0.0 else 1.0 / foundCandidates + + private fun getFirstLevelCandidates( + hints: List? + ): List { + val candidates = getInitCandidateMap() + hints?.forEach { hint -> + var isIter = false + val foundCandidates : Set = + when (hint) { + is ArgInfoCollector.Type -> + setOf(AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(hint.type)) + is ArgInfoCollector.Method -> { + if (hint.name == "__iter__") + isIter = true + PythonTypesStorage.findTypeWithMethod(hint.name) + } + is ArgInfoCollector.Field -> PythonTypesStorage.findTypeWithField(hint.name) + is ArgInfoCollector.FunctionArg -> { + PythonTypesStorage.findTypeByFunctionWithArgumentPosition( + hint.name, + argumentPosition = hint.index + ) + } + is ArgInfoCollector.FunctionRet -> PythonTypesStorage.findTypeByFunctionReturnValue(hint.name) + else -> emptySet() + } + val add = calcAdd(foundCandidates.size) + foundCandidates.forEach { increaseValue(candidates, it, add) } + if (isIter) + increaseForGenerics(candidates) + } + increaseForProjectClasses(candidates) + return candidatesMapToRating(candidates).take(MAX_CANDIDATES_FOR_PARAM) + } + + private fun getGeneralTypeRating( + argInfoCollector: ArgInfoCollector + ): List { + val candidates = getInitCandidateMap() + argInfoCollector.getAllGeneralHints().map { hint -> + val foundCandidates: Set = + when (hint) { + is ArgInfoCollector.Type -> + setOf(AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(hint.type)) + is ArgInfoCollector.Function -> + listOf( + PythonTypesStorage.findTypeByFunctionReturnValue(hint.name), + PythonTypesStorage.findTypeByFunctionWithArgumentPosition(hint.name) + ).flatten().toSet() + is ArgInfoCollector.Method -> PythonTypesStorage.findTypeWithMethod(hint.name) + is ArgInfoCollector.Field -> PythonTypesStorage.findTypeWithField(hint.name) + else -> emptySet() + } + val add = calcAdd(foundCandidates.size) + foundCandidates.forEach { increaseValue(candidates, it, add) } + } + increaseForProjectClasses(candidates) + return candidatesMapToRating(candidates).take(MAX_CANDIDATES_FOR_PARAM / 2) + } + + private fun getArgCandidates( + generalTypeRating: List, + argStorages: List = emptyList(), + userAnnotation: NormalizedPythonAnnotation? = null + ): List { + val root = + if (userAnnotation != null) + sequenceOf(userAnnotation).iterator() + else + getFirstLevelCandidates(argStorages).asSequence().iterator() + + val bfsQueue = ArrayDeque(listOf(root)) + val result = mutableListOf() + while (result.size < MAX_CANDIDATES_FOR_PARAM && bfsQueue.isNotEmpty()) { + val curIter = bfsQueue.removeFirst() + if (!curIter.hasNext()) + continue + + val value = curIter.next() + result.add(value) + bfsQueue.addLast(curIter) + + val asGeneric = parseGeneric(value) + if (asGeneric == null || !asGeneric.args.any { it == pythonAnyClassId }) + continue + + val argCandidates = asGeneric.args.map { + if (it == pythonAnyClassId) + generalTypeRating + else + listOf(it) + } + val toAnnotation = + when (asGeneric) { + is ListAnnotation -> ListAnnotation::pack + is DictAnnotation -> DictAnnotation::pack + is SetAnnotation -> SetAnnotation::pack + } + val nextGenericCandidates = PriorityCartesianProduct(argCandidates).getSequence().map { + NormalizedPythonAnnotation(toAnnotation(it).toString()) + } + bfsQueue.addFirst(nextGenericCandidates.iterator()) + } + return result + } + + private fun findTypeCandidates( + argInfoCollector: ArgInfoCollector, + existingAnnotations: Map + ): Map> { + val storageMap = argInfoCollector.getAllArgHints() + val generalTypeRating = getGeneralTypeRating(argInfoCollector) + val userAnnotations = existingAnnotations.entries.associate { + it.key to getArgCandidates(generalTypeRating, userAnnotation = it.value) + } + val annotationCombinations = storageMap.entries.associate { (name, storages) -> + name to getArgCandidates(generalTypeRating, storages) + } + return userAnnotations + annotationCombinations + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt new file mode 100644 index 0000000000..dd5f680430 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/GenericAnnotations.kt @@ -0,0 +1,90 @@ +package org.utbot.python.typing + +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation + +fun parseGeneric(annotation: NormalizedPythonAnnotation): GenericAnnotation? = + ListAnnotation.parse(annotation) + ?: DictAnnotation.parse(annotation) + ?: SetAnnotation.parse(annotation) + + +fun isGeneric(annotation: NormalizedPythonAnnotation): Boolean = parseGeneric(annotation) != null + + +sealed class GenericAnnotation { + abstract val args: List + + companion object { + fun getFromMatch(match: MatchResult, index: Int): NormalizedPythonAnnotation { + return NormalizedPythonAnnotation(match.groupValues[index]) + } + } +} + +class ListAnnotation( + val elemAnnotation: NormalizedPythonAnnotation +): GenericAnnotation() { + + override val args: List + get() = listOf(elemAnnotation) + + override fun toString(): String = "typing.List[$elemAnnotation]" + + companion object { + val regex = Regex("typing.List\\[(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): ListAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { + ListAnnotation(getFromMatch(it, 1)) + } + } + + fun pack(args: List) = ListAnnotation(args[0]) + } +} + +class DictAnnotation( + val keyAnnotation: NormalizedPythonAnnotation, + val valueAnnotation: NormalizedPythonAnnotation +): GenericAnnotation() { + + override val args: List + get() = listOf(keyAnnotation, valueAnnotation) + + override fun toString(): String = "typing.Dict[$keyAnnotation, $valueAnnotation]" + + companion object { + val regex = Regex("typing.Dict\\[(.*), *(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): DictAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { + DictAnnotation(getFromMatch(it, 1), getFromMatch(it, 2)) + } + } + + fun pack(args: List) = DictAnnotation(args[0], args[1]) + } +} + +class SetAnnotation( + val elemAnnotation: NormalizedPythonAnnotation +): GenericAnnotation() { + + override val args: List + get() = listOf(elemAnnotation) + + override fun toString(): String = "typing.Set[$elemAnnotation]" + + companion object { + val regex = Regex("typing.Set\\[(.*)]") + + fun parse(annotation: NormalizedPythonAnnotation): SetAnnotation? { + val res = regex.matchEntire(annotation.name) + return res?.let { SetAnnotation(getFromMatch(it, 1)) } + } + + fun pack(args: List) = SetAnnotation(args[0]) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt new file mode 100644 index 0000000000..54aa83eeba --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/MypyAnnotations.kt @@ -0,0 +1,149 @@ +package org.utbot.python.typing + +import mu.KotlinLogging +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.python.utils.Cleaner +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.PythonMethod +import org.utbot.python.code.PythonCodeGenerator.generateMypyCheckCode +import org.utbot.python.utils.PriorityCartesianProduct +import org.utbot.python.utils.getLineOfFunction +import org.utbot.python.utils.runCommand +import java.io.File + +private val logger = KotlinLogging.logger {} + +object MypyAnnotations { + const val TEMPORARY_MYPY_FILE = "" + + data class MypyReportLine( + val line: Int, + val type: String, + val message: String, + val file: String + ) + + fun getCheckedByMypyAnnotations( + method: PythonMethod, + functionArgAnnotations: Map>, + moduleToImport: String, + directoriesForSysPath: Set, + pythonPath: String, + isCancelled: () -> Boolean, + storageForMypyMessages: MutableList? = null + ) = sequence { + val fileWithCode = TemporaryFileManager.assignTemporaryFile(tag = "mypy.py") + val codeWithoutAnnotations = generateMypyCheckCode( + method, + emptyMap(), + directoriesForSysPath, + moduleToImport + ) + TemporaryFileManager.writeToAssignedFile(fileWithCode, codeWithoutAnnotations) + val configFile = setConfigFile(directoriesForSysPath) + Cleaner.addFunction { stopMypy(pythonPath) } + + logger.debug("First mypy run") + val defaultOutputAsString = mypyCheck(pythonPath, fileWithCode, configFile) + val defaultErrorsAndNotes = getErrorsAndNotes(defaultOutputAsString, codeWithoutAnnotations, fileWithCode) + + if (storageForMypyMessages != null) { + defaultErrorsAndNotes.forEach { storageForMypyMessages.add(it) } + } + + val defaultErrorNum = getErrorNumber(defaultErrorsAndNotes) + + val candidates = functionArgAnnotations.entries.map { (key, value) -> + value.map { + Pair(key, it) + } + } + if (candidates.any { it.isEmpty() }) { + return@sequence + } + + PriorityCartesianProduct(candidates).getSequence().forEach { generatedAnnotations -> + if (isCancelled()) { + return@sequence + } + + logger.debug("Checking annotations: ${ + generatedAnnotations.joinToString { "${it.first}: ${it.second}" } + }") + + val annotationMap = generatedAnnotations.toMap() + val codeWithAnnotations = generateMypyCheckCode( + method, + annotationMap, + directoriesForSysPath, + moduleToImport + ) + TemporaryFileManager.writeToAssignedFile(fileWithCode, codeWithAnnotations) + val mypyOutputAsString = mypyCheck(pythonPath, fileWithCode, configFile) + val mypyOutput = getErrorsAndNotes(mypyOutputAsString, codeWithAnnotations, fileWithCode) + val errorNum = getErrorNumber(mypyOutput) + + if (errorNum <= defaultErrorNum) { + yield(annotationMap.mapValues { entry -> + entry.value + }) + } + } + } + + private const val configFilename = "mypy.ini" + + private fun setConfigFile(directoriesForSysPath: Set): File { + val file = TemporaryFileManager.assignTemporaryFile(configFilename) + val configContent = """ + [mypy] + mypy_path = ${directoriesForSysPath.joinToString(separator = ":")} + namespace_packages = True + explicit_package_bases = True + show_absolute_path = True + """.trimIndent() + TemporaryFileManager.writeToAssignedFile(file, configContent) + return file + } + + private fun stopMypy(pythonPath: String): Int { + val result = runCommand(listOf( + pythonPath, + "-m", + "mypy.dmypy", + "stop" + )) + return result.exitValue + } + + private fun mypyCheck(pythonPath: String, fileWithCode: File, configFile: File): String { + val result = runCommand(listOf( + pythonPath, + "-m", + "mypy.dmypy", + "run", + "--", + fileWithCode.path, + "--config-file", + configFile.path + )) + return result.stdout + } + + private fun getErrorNumber(mypyReport: List) = + mypyReport.count { it.type == "error" && it.file == TEMPORARY_MYPY_FILE } + + private fun getErrorsAndNotes(mypyOutput: String, mypyCode: String, fileWithCode: File): List { + val regex = Regex("(?m)^([^\n]*):([0-9]*): (error|note): ([^\n]*)\n") + return regex.findAll(mypyOutput).toList().map { match -> + val file = match.groupValues[1] + MypyReportLine( + match.groupValues[2].toInt() - getLineOfFunction(mypyCode)!!, + match.groupValues[3], + match.groupValues[4], + if (file == fileWithCode.path) TEMPORARY_MYPY_FILE else file + ) + } + } +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt new file mode 100644 index 0000000000..f2d4a47881 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/PythonTypeCollector.kt @@ -0,0 +1,231 @@ +package org.utbot.python.typing + +import com.beust.klaxon.Klaxon +import mu.KotlinLogging +import org.apache.commons.io.FileUtils +import org.apache.commons.io.IOUtils +import org.apache.commons.io.filefilter.FileFilterUtils +import org.apache.commons.io.filefilter.NameFileFilter +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.python.code.ClassInfoCollector +import org.utbot.python.code.PythonClass +import org.utbot.python.code.PythonCode +import org.utbot.python.code.PythonModule +import org.utbot.python.utils.AnnotationNormalizer +import org.utbot.python.utils.AnnotationNormalizer.annotationFromProjectToClassId +import org.utbot.python.utils.checkIfFileLiesInPath +import org.utbot.python.utils.getModuleNameWithoutCheck +import java.io.File +import java.io.FileInputStream +import java.nio.charset.StandardCharsets + +private val logger = KotlinLogging.logger {} + +class PythonClassIdInfo( + val pythonClassId: PythonClassId, + val initSignature: List?, + val preprocessedInstances: List?, + val methods: Set, + val fields: Set +) + +object PythonTypesStorage { + var projectClasses: List = emptyList() + var projectModules: List = emptyList() + var pythonPath: String? = null + private const val PYTHON_NOT_SPECIFIED = "PythonPath in PythonTypeCollector not specified" + + fun findTypeWithMethod( + methodName: String + ): Set { + val fromStubs = StubFileFinder.findTypeWithMethod(methodName).map { + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it) + } + val fromProject = projectClasses.mapNotNull { + if (it.info.methods.contains(methodName)) + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it.name) + else + null + } + return (fromStubs union fromProject).toSet() + } + + fun findTypeWithField( + fieldName: String + ): Set { + val fromStubs = StubFileFinder.findTypeWithField(fieldName).map { + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it) + } + val fromProject = projectClasses.mapNotNull { + if (it.info.fields.contains(fieldName)) + AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(it.name) + else + null + } + return (fromStubs union fromProject).toSet() + } + + fun findTypeByFunctionWithArgumentPosition( + functionName: String, + argumentName: String? = null, + argumentPosition: Int? = null, + ): Set = + StubFileFinder.findAnnotationByFunctionWithArgumentPosition(functionName, argumentName, argumentPosition) + + fun findTypeByFunctionReturnValue(functionName: String): Set = + StubFileFinder.findAnnotationByFunctionReturnValue(functionName).toSet() + + fun isClassFromProject(typeName: NormalizedPythonAnnotation): Boolean { + return projectClasses.any { it.name.name == typeName.name } + } + + fun findPythonClassIdInfoByName(classIdName: String): PythonClassIdInfo? { + val fromStub = StubFileFinder.nameToClassMap[classIdName] + val result = + if (fromStub != null) { + val fromPreprocessed = TypesFromJSONStorage.typeNameMap[classIdName] + val classId = PythonClassId(fromStub.className) + return PythonClassIdInfo( + classId, + fromStub.methods.find { it.name == "__init__" } + ?.args + ?.drop(1) // drop 'self' parameter + ?.map { NormalizedPythonAnnotation(it.annotation) }, + fromPreprocessed?.instances, + fromStub.methods.map { it.name }.toSet(), + fromStub.fields.map { it.name }.toSet() + ) + } else { + projectClasses.find { it.name.name == classIdName } ?.let { projectClass -> + PythonClassIdInfo( + projectClass.name, + projectClass.initAnnotation, + null, + projectClass.info.methods, + projectClass.info.fields + ) + } + } + + return result + } + + val builtinTypes: List + get() = TypesFromJSONStorage.preprocessedTypes.mapNotNull { + if (it.name.startsWith("builtins.")) it.name.removePrefix("builtins.") else null + } + + data class ProjectClass( + val pythonClass: PythonClass, + val info: ClassInfoCollector.Storage, + val initAnnotation: List?, + val name: PythonClassId + ) + + private fun getPythonFiles(directory: File): Collection = + FileUtils.listFiles( + directory, + /* fileFilter = */ FileFilterUtils.and( + FileFilterUtils.suffixFileFilter(".py"), + FileFilterUtils.notFileFilter( + FileFilterUtils.prefixFileFilter("test") + ), + ), + /* dirFilter = */ FileFilterUtils.and( + FileFilterUtils.notFileFilter( + NameFileFilter("test") + ), + FileFilterUtils.notFileFilter( + FileFilterUtils.suffixFileFilter("venv") + ), + FileFilterUtils.notFileFilter( + FileFilterUtils.prefixFileFilter(".") + ) + ) + ) + + fun refreshProjectClassesAndModulesLists( + directoriesForSysPath: Set, + onlyFromSpecifiedFile: File? = null + ) { + val projectClassesSet = mutableSetOf() + val projectModulesSet = mutableSetOf(PythonModule("builtins")) + + val filesToVisit = directoriesForSysPath.flatMap { path -> + if (onlyFromSpecifiedFile != null && !checkIfFileLiesInPath(path, onlyFromSpecifiedFile.path)) + return@flatMap emptyList() + + val pathFile = File(path) + if (onlyFromSpecifiedFile != null) + return@flatMap listOf( + Pair(getModuleNameWithoutCheck(pathFile, onlyFromSpecifiedFile), onlyFromSpecifiedFile) + ) + + getPythonFiles(pathFile).map { Pair(getModuleNameWithoutCheck(pathFile, it), it) } + } .distinctBy { it.second } + + filesToVisit.forEach { (module, file) -> + val content = IOUtils.toString(FileInputStream(file), StandardCharsets.UTF_8) + val code = PythonCode.getFromString(content, file.path) ?: return@forEach + projectClassesSet += code.getToplevelClasses().map { pyClass -> + val collector = ClassInfoCollector(pyClass) + val initSignature = pyClass.initSignature + ?.map { + annotationFromProjectToClassId( + it.annotation, + pythonPath ?: error(PYTHON_NOT_SPECIFIED), + module, + pyClass.filename!!, + directoriesForSysPath + ) + } + val fullClassName = module + "." + pyClass.name + ProjectClass(pyClass, collector.storage, initSignature, PythonClassId(fullClassName)) + } + projectModulesSet += code.getToplevelModules() + } + projectClasses = projectClassesSet.toList() + + val newModules = projectModulesSet - projectModules.toSet() + + logger.debug("Updating info from stub files") + + updateStubFiles(newModules.map { it.name } .toList()) + projectModules = projectModulesSet.toList() + } + + private fun updateStubFiles(newModules: List) { + if (newModules.isNotEmpty()) { + val jsonData = StubFileReader.getStubInfo( + newModules, + pythonPath ?: error(PYTHON_NOT_SPECIFIED), + ) + StubFileFinder.updateStubs(jsonData) + } + } + + + private data class PreprocessedValueFromJSON( + val name: String, + val instances: List + ) + + private object TypesFromJSONStorage { + val preprocessedTypes: List + init { + val typesAsString = PythonTypesStorage::class.java.getResource("/preprocessed_values.json") + ?.readText(Charsets.UTF_8) + ?: error("Didn't find preprocessed_values.json") + preprocessedTypes = Klaxon().parseArray(typesAsString) ?: emptyList() + } + + val typeNameMap: Map by lazy { + val result = mutableMapOf() + preprocessedTypes.forEach { type -> + result[type.name] = type + } + result + } + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt new file mode 100644 index 0000000000..e821d62701 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileFinder.kt @@ -0,0 +1,150 @@ +package org.utbot.python.typing + +import com.squareup.moshi.Moshi +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonClassId + +object StubFileFinder { + val methodToTypeMap: MutableMap> = mutableMapOf() + val functionToTypeMap: MutableMap> = mutableMapOf() + val fieldToTypeMap: MutableMap> = mutableMapOf() + val nameToClassMap: MutableMap = mutableMapOf() + + private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build() + private val jsonAdapter = moshi.adapter(StubFileStructures.JsonData::class.java) + + private fun parseJson(json: String): StubFileStructures.JsonData? { + return if (json.isNotEmpty()) { + jsonAdapter.fromJson(json) + } else { + null + } + } + + fun updateStubs( + json: String + ) { + val jsonData = parseJson(json) + if (jsonData != null) { + jsonData.normalizeAnnotations() + updateMethods(jsonData.methodAnnotations) + updateFields(jsonData.fieldAnnotations) + updateFunctions(jsonData.functionAnnotations) + updateClasses(jsonData.classAnnotations) + } + } + + private fun updateMethods(newMethods: List) { + newMethods.forEach { function -> + if (!methodToTypeMap.containsKey(function.name)) + methodToTypeMap[function.name] = function.definitions.toMutableSet() + else + methodToTypeMap[function.name]?.addAll(function.definitions) + } + } + + private fun updateFunctions(newFunctions: List) { + newFunctions.forEach { function -> + if (!functionToTypeMap.containsKey(function.name)) + functionToTypeMap[function.name] = function.definitions.toMutableSet() + else + functionToTypeMap[function.name]?.addAll(function.definitions) + } + } + + private fun updateFields(newFields: List) { + newFields.forEach { field -> + if (!fieldToTypeMap.containsKey(field.name)) + fieldToTypeMap[field.name] = field.definitions.toMutableSet() + else + fieldToTypeMap[field.name]?.addAll(field.definitions) + } + } + + private fun updateClasses(newClasses: List) { + newClasses.forEach { pyClass -> + nameToClassMap[pyClass.className] = pyClass + } + } + + fun findTypeWithMethod( + methodName: String + ): Set { + return (methodToTypeMap[methodName] ?: emptyList()).mapNotNull { methodInfo -> + methodInfo.className?.let { PythonClassId(it) } + }.toSet() + } + + fun findTypeWithField( + fieldName: String + ): Set { + return (fieldToTypeMap[fieldName] ?: emptyList()).map { + PythonClassId(it.className) + }.toSet() + } + + fun findAnnotationByFunctionWithArgumentPosition( + functionName: String, + argumentName: String? = null, + argumentPosition: Int? = null, + ): Set { + val functionInfos = functionToTypeMap[functionName] ?: emptyList() + val types = mutableSetOf() + if (argumentName != null) { + functionInfos.forEach { functionInfo -> + (functionInfo.args + functionInfo.kwonlyargs).forEach { + if (it.arg == argumentName && it.annotation != "") + types.add(NormalizedPythonAnnotation(it.annotation)) + } + } + } else if (argumentPosition != null) { + functionInfos.forEach { functionInfo -> + val checkCountArgs = functionInfo.args.size > argumentPosition + val ann = functionInfo.args.getOrNull(argumentPosition)?.annotation ?: "" + if (checkCountArgs && ann != "") { + types.add(NormalizedPythonAnnotation(ann)) + } + } + } else { + functionInfos.forEach { functionInfo -> + functionInfo.args.forEach { + if (it.annotation != "") + types.add(NormalizedPythonAnnotation(it.annotation)) + } + } + } + return types + } + + fun findAnnotationByFunctionReturnValue(functionName: String): Set { + return functionToTypeMap[functionName]?.map { + NormalizedPythonAnnotation(it.returns) + }?.toSet() ?: emptySet() + } +} + +fun main() { + println(StubFileFinder.findTypeWithMethod("__add__")) + println( + StubFileFinder.findAnnotationByFunctionWithArgumentPosition( + "print", "sep" + ) + ) + println( + StubFileFinder.findAnnotationByFunctionWithArgumentPosition( + "heapify", argumentPosition = 0 + ) + ) + println( + StubFileFinder.findAnnotationByFunctionWithArgumentPosition( + "gt" + ) + ) + println( + StubFileFinder.findTypeWithField( + "value" + ) + ) +} + diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt new file mode 100644 index 0000000000..8733eb9cf8 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileReader.kt @@ -0,0 +1,24 @@ +package org.utbot.python.typing + +import org.utbot.python.utils.TemporaryFileManager +import org.utbot.python.utils.runCommand + +object StubFileReader { + private const val scriptPath = "/typeshed_stub.py" + + fun getStubInfo( + modules: List, + pythonPath: String, + ): String { + val scriptContent = StubFileFinder::class.java.getResource(scriptPath)?.readText() ?: error("Didn't find $scriptPath") + val scriptFile = TemporaryFileManager.createTemporaryFile(scriptContent, tag="stub_file_reader") + + val command = + listOf( + pythonPath, + scriptFile.absolutePath, + ) + modules + val result = runCommand(command) + return result.stdout + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt new file mode 100644 index 0000000000..bf5ecb413c --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/typing/StubFileStructures.kt @@ -0,0 +1,112 @@ +package org.utbot.python.typing + +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.python.utils.AnnotationNormalizer + +object StubFileStructures { + + data class JsonData( + val classAnnotations: List, + val fieldAnnotations: List, + val functionAnnotations: List, + val methodAnnotations: List, + ) { + fun normalizeAnnotations() { + classAnnotations.forEach { clazz -> + clazz.normalizeAnnotations() + } + fieldAnnotations.forEach { field -> + field.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + functionAnnotations.forEach { function -> + function.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + methodAnnotations.forEach { method -> + method.definitions.forEach { def -> + def.normalizeAnnotations() + } + } + } + } + + data class FieldIndex( + val name: String, + val definitions: List + ) + + data class FunctionIndex( + val name: String, + val definitions: List + ) + + data class MethodIndex( + val name: String, + val definitions: List + ) + + data class FieldInfo( + var annotation: String, // must be NormalizedAnnotation + val className: String, // must be PythonClassId + val name: String, + ) { + fun normalizeAnnotations() { + this.annotation = getNormalAnnotation(this.annotation) + } + } + + data class ClassInfo( + val className: String, // must be PythonClassId + val fields: List, + val methods: List, + ) { + fun normalizeAnnotations() { + this.fields.forEach { field -> + field.normalizeAnnotations() + } + this.methods.forEach { method -> + method.normalizeAnnotations() + } + } + } + + data class FunctionInfo( + val className: String?, // must be PythonClassId? + val args: List = emptyList(), + val kwonlyargs: List = emptyList(), + val name: String, + var returns: String, // must be NormalizedAnnotation + ) { + val defName: String + get() = name.split('.').last() + + val module: String + get() = name.split('.').dropLast(1).joinToString(".") + + fun normalizeAnnotations() { + this.args.forEach { arg -> + arg.normalizeAnnotations() + } + this.kwonlyargs.forEach { kwarg -> + kwarg.normalizeAnnotations() + } + this.returns = getNormalAnnotation(this.returns) + } + } + + data class ArgInfo( + val arg: String, + var annotation: String, // must be NormalizedAnnotation + ) { + fun normalizeAnnotations() { + this.annotation = getNormalAnnotation(this.annotation) + } + } + + fun getNormalAnnotation(annotation: String): String { + return AnnotationNormalizer.pythonClassIdToNormalizedAnnotation(PythonClassId(annotation)).name + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt new file mode 100644 index 0000000000..7ed37d6372 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/AnnotationNormalizer.kt @@ -0,0 +1,92 @@ +package org.utbot.python.utils + +import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation +import org.utbot.framework.plugin.api.python.PythonClassId +import org.utbot.framework.plugin.api.python.pythonAnyClassId +import java.io.File + + +object AnnotationNormalizer { + private val scriptContent = AnnotationNormalizer::class.java + .getResource("/normalize_annotation_from_project.py") + ?.readText() + ?: error("Didn't find /normalize_annotation_from_project.py") + + private var normalizeAnnotationFromProjectScript_: File? = null + private val normalizeAnnotationFromProjectScript: File + get() { + val result = normalizeAnnotationFromProjectScript_ + if (result == null || !result.exists()) { + val result1 = TemporaryFileManager.createTemporaryFile(scriptContent, tag = "normalize_annotation.py") + normalizeAnnotationFromProjectScript_ = result1 + return result1 + } + return result + } + + private fun normalizeAnnotationFromProject( + annotation: String, + pythonPath: String, + curPythonModule: String, + fileOfAnnotation: String, + filesToAddToSysPath: Set + ): String { + val result = runCommand( + listOf( + pythonPath, + normalizeAnnotationFromProjectScript.path, + annotation, + curPythonModule, + fileOfAnnotation, + ) + filesToAddToSysPath, + ) + return if (result.exitValue == 0) result.stdout else annotation + } + + fun annotationFromProjectToClassId( + annotation: String?, + pythonPath: String, + curPythonModule: String, + fileOfAnnotation: String, + filesToAddToSysPath: Set + ): NormalizedPythonAnnotation = + if (annotation == null) + pythonAnyClassId + else + NormalizedPythonAnnotation( + substituteTypes( + normalizeAnnotationFromProject( + annotation, + pythonPath, + curPythonModule, + fileOfAnnotation, + filesToAddToSysPath + ) + ) + ) + + private val substitutionMapFirstStage = listOf( + "builtins.list" to "typing.List", + "builtins.dict" to "typing.Dict", + "builtins.set" to "typing.Set" + ) + + private val substitutionMapSecondStage = listOf( + Regex("typing.List *([^\\[]|$)") to "typing.List[typing.Any]", + Regex("typing.Dict *([^\\[]|$)") to "typing.Dict[typing.Any, typing.Any]", + Regex("typing.Set *([^\\[]|$)") to "typing.Set[typing.Any]" + ) + + private fun substituteTypes(annotation: String): String { + val firstStage = substitutionMapFirstStage.fold(annotation) { acc, (old, new) -> + acc.replace(old, new) + } + return substitutionMapSecondStage.fold(firstStage) { acc, (re, new) -> + acc.replace(re, new) + } + } + + fun pythonClassIdToNormalizedAnnotation(classId: PythonClassId): NormalizedPythonAnnotation { + return NormalizedPythonAnnotation(substituteTypes(classId.name)) + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt new file mode 100644 index 0000000000..cc29c36562 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/Cleaner.kt @@ -0,0 +1,22 @@ +package org.utbot.python.utils + +object Cleaner { + private var clean: () -> Unit = {} + + fun addFunction(f: () -> Unit) { + val oldClean = clean + val newClean = { + f() + oldClean() + } + clean = newClean + } + + fun restart() { + clean = {} + } + + fun doCleaning() { + clean() + } +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt new file mode 100644 index 0000000000..a34e34a9c3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/PriorityCartesianProduct.kt @@ -0,0 +1,41 @@ +package org.utbot.python.utils + +import java.lang.Integer.min + +class PriorityCartesianProduct(private val lists: List>){ + + private fun generateFixedSumRepresentation( + sum: Int, + index: Int = 0, + curRepr: List = emptyList() + ): Sequence> { + val itemNumber = lists.size + var result = emptySequence>() + if (index == itemNumber && sum == 0) { + return sequenceOf(curRepr) + } else if (index < itemNumber && sum >= 0) { + for (i in 0..min(sum, lists[index].size - 1)) { + result += generateFixedSumRepresentation( + sum - i, + index + 1, + curRepr + listOf(i) + ) + } + } + return result + } + + fun getSequence(): Sequence> { + var curSum = 0 + val maxSum = lists.fold(0) { acc, elem -> acc + elem.size } + val combinations = generateSequence { + if (curSum > maxSum) + null + else + generateFixedSumRepresentation(curSum++) + } + return combinations.flatten().map { combination: List -> + combination.mapIndexed { element, value -> lists[element][value] } + } + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt new file mode 100644 index 0000000000..d39dd3d7d3 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/ProcessUtils.kt @@ -0,0 +1,43 @@ +package org.utbot.python.utils + +import java.io.BufferedReader +import java.io.InputStreamReader +import java.util.concurrent.TimeUnit + + +data class CmdResult( + val stdout: String, + val stderr: String, + val exitValue: Int, + val terminatedByTimeout: Boolean = false +) + +fun startProcess(command: List): Process = ProcessBuilder(command).start() + +fun getResult(process: Process, timeout: Long? = null): CmdResult { + if (timeout != null) { + if (!process.waitFor(timeout, TimeUnit.MILLISECONDS)) { + process.destroy() + return CmdResult("", "", 1, terminatedByTimeout = true) + } + } + + val reader = BufferedReader(InputStreamReader(process.inputStream)) + var stdout = "" + var line: String? = "" + while (line != null) { + stdout += "$line\n" + line = reader.readLine() + } + + if (timeout == null) + process.waitFor() + + val stderr = process.errorStream.readBytes().decodeToString().trimIndent() + return CmdResult(stdout.trimIndent(), stderr, process.exitValue()) +} + +fun runCommand(command: List, timeout: Long? = null): CmdResult { + val process = startProcess(command) + return getResult(process, timeout) +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt new file mode 100644 index 0000000000..39762a5f2f --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/RequirementsUtils.kt @@ -0,0 +1,37 @@ +package org.utbot.python.utils + +object RequirementsUtils { + val requirements: List = + RequirementsUtils::class.java.getResource("/requirements.txt") + ?.readText() + ?.split('\n') + ?.filter { it.isNotEmpty() } + ?: error("Didn't find /requirements.txt") + + private val requirementsScriptContent: String = + RequirementsUtils::class.java.getResource("/check_requirements.py") + ?.readText() + ?: error("Didn't find /check_requirements.py") + + fun requirementsAreInstalled(pythonPath: String): Boolean { + val requirementsScript = TemporaryFileManager.createTemporaryFile(requirementsScriptContent, tag = "requirements") + val result = runCommand( + listOf( + pythonPath, + requirementsScript.path + ) + requirements + ) + return result.exitValue == 0 + } + + fun installRequirements(pythonPath: String): CmdResult { + return runCommand( + listOf( + pythonPath, + "-m", + "pip", + "install" + ) + requirements + ) + } +} diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt new file mode 100644 index 0000000000..f36df26968 --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/StringUtils.kt @@ -0,0 +1,48 @@ +package org.utbot.python.utils + +import org.utbot.common.PathUtil.toPath +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +// numeration from zero +fun getLineNumber(content: String, pos: Int) = + content.substring(0, pos).count { it == '\n' } + +fun getLineOfFunction(code: String, functionName: String? = null): Int? { + val regex = + if (functionName != null) + """(?m)^def +$functionName\(""".toRegex() + else + """(?m)^def""".toRegex() + + val trimmedCode = code.replaceIndent() + return regex.find(trimmedCode)?.range?.first?.let { getLineNumber(trimmedCode, it) } +} + +fun String.camelToSnakeCase(): String { + val camelRegex = "(?<=[a-zA-Z])[\\dA-Z]".toRegex() + return camelRegex.replace(this) { + "_${it.value}" + }.lowercase() +} + +fun moduleOfType(typeName: String): String? { + val lastIndex = typeName.lastIndexOf('.') + return if (lastIndex == -1) null else typeName.substring(0, lastIndex) +} + +fun checkIfFileLiesInPath(path: String, fileWithClassPath: String): Boolean { + val parentPath = Paths.get(path) + val childPath = Paths.get(fileWithClassPath) + return childPath.startsWith(parentPath) +} + +fun getModuleNameWithoutCheck(path: File, fileWithClass: File): String = + path.toURI().relativize(fileWithClass.toURI()).path.removeSuffix(".py").toPath().joinToString(".") + +fun getModuleName(path: String, fileWithClassPath: String): String? { + if (checkIfFileLiesInPath(path, fileWithClassPath)) + return getModuleNameWithoutCheck(File(path), File(fileWithClassPath)) + return null +} \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt new file mode 100644 index 0000000000..90e061d81e --- /dev/null +++ b/utbot-python/src/main/kotlin/org/utbot/python/utils/TemporaryFileManager.kt @@ -0,0 +1,49 @@ +package org.utbot.python.utils + +import org.utbot.common.FileUtil +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.deleteExisting + +object TemporaryFileManager { + private var testSourceRoot: String = "" + private lateinit var tmpDirectory: Path + private var nextId = 0 + + fun setup(testSourceRoot: String) { + tmpDirectory = FileUtil.createTempDirectory("python-test-generation") + Cleaner.addFunction { tmpDirectory.deleteExisting() } + + this.testSourceRoot = testSourceRoot + val testsFolder = File(testSourceRoot) + if (!testsFolder.exists()) + testsFolder.mkdirs() + } + + fun assignTemporaryFile(fileName_: String? = null, tag: String? = null, addToCleaner: Boolean = true): File { + val fileName = fileName_ ?: ("${nextId++}_" + (tag ?: "")) + val fullpath = Paths.get(tmpDirectory.toString(), fileName) + val result = fullpath.toFile() + if (addToCleaner) + Cleaner.addFunction { result.delete() } + return result + } + + fun writeToAssignedFile(file: File, content: String) { + file.writeText(content) + file.parentFile?.mkdirs() + file.createNewFile() + } + + fun createTemporaryFile( + content: String, + fileName: String? = null, + tag: String? = null, + addToCleaner: Boolean = true + ): File { + val file = assignTemporaryFile(fileName, tag, addToCleaner) + writeToAssignedFile(file, content) + return file + } +} \ No newline at end of file diff --git a/utbot-python/src/main/resources/check_requirements.py b/utbot-python/src/main/resources/check_requirements.py new file mode 100644 index 0000000000..e1b466d627 --- /dev/null +++ b/utbot-python/src/main/resources/check_requirements.py @@ -0,0 +1,7 @@ +import pkg_resources +import sys + + +if __name__ == "__main__": + dependencies = sys.argv[1:] + pkg_resources.require(dependencies) diff --git a/utbot-python/src/main/resources/normalize_annotation_from_project.py b/utbot-python/src/main/resources/normalize_annotation_from_project.py new file mode 100644 index 0000000000..5d3e29d5f7 --- /dev/null +++ b/utbot-python/src/main/resources/normalize_annotation_from_project.py @@ -0,0 +1,50 @@ +import importlib.machinery +import inspect +import sys +import types +import mypy.fastparse + + +def main(annotation: str, cur_module: str, path: str): + def walk_mypy_type(mypy_type) -> str: + try: + source = inspect.getfile(eval(mypy_type.name)) + # in_project = source.startswith(project_root) + except: + None + + modname = eval(mypy_type.name).__module__ + simple_name = mypy_type.name.split('.')[-1] + fullname = f'{modname}.{simple_name}' + + result = fullname + if len(mypy_type.args) != 0: + arg_strs = [ + walk_mypy_type(arg) + for arg in mypy_type.args + ] + result += f"[{', '.join(arg_strs)}]" + return result + + loader = importlib.machinery.SourceFileLoader(cur_module, path) + mod = types.ModuleType(loader.name) + loader.exec_module(mod) + + for name in dir(mod): + globals()[name] = getattr(mod, name) + + mypy_type_ = mypy.fastparse.parse_type_string(annotation, annotation, -1, -1) + print(walk_mypy_type(mypy_type_), end='') + + +def get_args(): + annotation = sys.argv[1] + cur_module = sys.argv[2] + path = sys.argv[3] + for extra_path in sys.argv[4:]: + sys.path.append(extra_path) + return annotation, cur_module, path + + +if __name__ == '__main__': + main(*get_args()) diff --git a/utbot-python/src/main/resources/preprocessed_values.json b/utbot-python/src/main/resources/preprocessed_values.json new file mode 100644 index 0000000000..4174022ed9 --- /dev/null +++ b/utbot-python/src/main/resources/preprocessed_values.json @@ -0,0 +1,997 @@ +[ + { + "name": "builtins.int", + "instances": [ + "0", + "1", + "-1", + "4294967297", + "4294967296", + "(1 << 100)", + "83", + "123", + "-3", + "100", + "10", + "314", + "int('281d55i5', 21)", + "int(' 0B100 ', 0)", + "int('1_00', 3)", + "int('32244002423141', 5)", + "int('10000000000000001', 4)", + "int('4f5aff66', 19)", + "int('mb994ag', 24)", + "int('1' * 600)", + "int('40000000000', 8)", + "int(memoryview(b'1234')[1:3])", + "int('1904440554', 11)", + "int('3723ai4g', 20)", + "int(' 0X123 ', 0)", + "int('100000000000000000000000000000001', 2)", + "int(memoryview(b'123 ')[1:3])", + "int('1_2_3_4_5_6_7_8_9', 16)", + "int('a7ffda91', 17)", + "int('0x123', 0)", + "int('0B100', 2)", + "int('0O123', 8)", + "int('102002022201221111211', 3)", + "int('4f5aff67', 19)", + "int('8pfgih4', 28)", + "int(1e+100)", + "int(b'-1')", + "int('dnchbnn', 26)", + "int(' 0o123 ', 0)", + "int('0o123', 8)", + "int('1z141z5', 36)", + "int('12068657455', 9)", + "int('1_2_3_4_5_6_7', 32)", + "int('2ca5b7464', 14)", + "int('9ba461595', 12)", + "int('4q0jto5', 31)", + "int('b28jpdn', 27)", + "int('5qmcpqg', 30)", + "int(3.14)", + "int('12068657454', 9)", + "int('3aokq95', 33)", + "int('211301422354', 7)", + "int('2qhxjlj', 34)", + "int('1fj8b184', 22)", + "int('\\u2003-3\\u2002')", + "int('1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1_0_1', 2)", + "int('0b100', 0)", + "int('hek2mgm', 25)", + "int('0X123', 16)", + "int('1904440555', 11)" + ] + }, + { + "name": "builtins.bool", + "instances": [ + "True", + "False" + ] + }, + { + "name": "builtins.str", + "instances": [ + "str(1.5 + 3.5j)", + "str()", + "str(b'\\xf0\\xa3\\x91\\x96', 'utf-8')", + "str(-1234567890)", + "str(1e+300 * 1e+300)", + "str(b'\\x80')", + "str(-123456789)", + "str(id)", + "str('unicode remains unicode')", + "str(3 + 0.0j)", + "str(object=500)", + "str(3.0j)", + "str('strings are converted to unicode')", + "str(b'foo', errors='strict')", + "str(b'xn--pythn-mua.org.', 'idna')", + "str(1 + 3.0j)", + "str(OSError(1001))", + "str(bytearray(b''))", + "str(b'\\xf0\\x90\\x80\\x82', 'utf-8')", + "str(['1', '2', '3'])", + "str(True)", + "str('abcdefghijklmnopqrst')", + "str(b'\\xe2\\x82\\xac', 'utf-8')", + "str(b'x')", + "str('a = 1')", + "str(2 ** 1000)", + "str(bytearray(b'x'))", + "str(3.2 + 0.0j)", + "str(6442450944)", + "str('3')", + "str('global')", + "str(False)" + ] + }, + { + "name": "builtins.float", + "instances": [ + "float(-1)", + "0.0", + "float('nan')", + "float('1.4')", + "float('+infinity')", + "float(10 ** 23)", + "7.3", + "float(1970)", + "float(314)", + "float(1)", + "float(-2100.0)", + "float(2 ** 31)", + "float(2 ** 64)", + "float('-INFINITY')", + "float(1 - 2 ** 31)", + "-2.1", + "float(2 ** 63)", + "3.14", + "3.2e3", + "2.1", + "float(-2 ** 63)", + "2.5", + "0.5", + "float(2 ** 31 - 1)", + "float('-nan')", + "3.e3", + "float(-2 ** 31)", + "float(-7.3)", + "float(2 ** 34)", + "float(2 ** 32)", + "1.23e+300" + ] + }, + { + "name": "builtins.range", + "instances": [ + "range(127, 256)", + "range(24)", + "range(0, 2 ** 100 + 1, 2)", + "range(1, 25 + 1)", + "range(0, 3)", + "range(-10, 10)", + "range(10, -11, -1)", + "range(240)", + "range(2 ** 200, 2 ** 201, 2 ** 100)", + "range(200)", + "range(50, 400)", + "range(150)", + "range(9, -1, -2)", + "range(3 * 5 * 7 * 11)", + "range(4, 16)", + "range(0, 2 ** 100 - 1, 2)", + "range(0, 55296)", + "range(101)", + "range(5000)", + "range(65536, 1114112)", + "range((1 << 16) - 1)", + "range(1500)", + "range(1, 9)", + "range(512)", + "range(0, -20, -1)", + "range(32, 127)", + "range(52, 64)", + "range(1 << 1000)", + "range(70000)" + ] + }, + { + "name": "builtins.complex", + "instances": [ + "complex(1.0, float('inf'))", + "complex('1j')", + "complex(1.0, 10.0)", + "complex(0.0j, 3.14)", + "complex('(1+2j)')", + "complex(float('inf'), float('inf'))", + "complex('1' * 500)", + "complex(float('inf'), -1)", + "complex(0.0, float('nan'))", + "complex(10.0)", + "complex(float('nan'), 1)", + "complex(1.0, 0.0)", + "complex(0, 0)", + "complex('(1.3+2.2j)')", + "complex(3.14 + 0.0j)", + "complex(float('nan'), -1)", + "complex(0.0, -float('inf'))", + "complex(repr(-6.0j))", + "complex(real=17 + 23.0j)", + "complex('( j )')", + "complex('-1e500+1.8e308j')", + "complex(float('inf'), 0)", + "complex(0, float('nan'))", + "complex('1e500')", + "complex('-1e-500j')", + "complex(10)", + "complex(' ( +3.14-6J )')", + "complex(1, 10)", + "complex(0.0, 0.0)", + "complex(3.14 + 0.0j, 0.0j)", + "complex(1, float('inf'))", + "complex(314, 0)", + "complex('-1e-500+1e-500j')", + "complex(3.14, 0.0)", + "complex(-float('inf'), float('inf'))", + "complex('( -j)')", + "complex(10 + 0.0j)", + "complex(-0.0, 0.0)", + "complex(5.3, 9.8)", + "complex(1.0, -float('inf'))", + "complex('-1e500j')", + "complex(0.0j, 3.14j)", + "complex(0.0, -1.0)", + "complex(real=17 + 23.0j, imag=23)", + "complex(0.0, -0.0)", + "complex('-1')", + "complex(repr(6.0j))", + "complex(1e-200, 1e-200)", + "complex(314)", + "complex(repr(1 + 6.0j))", + "complex(-0.0, -1.0)", + "complex(float('inf'), 0.0)", + "complex(0, -float('inf'))", + "complex()", + "complex(0.0, 1.0)", + "complex('+1')", + "complex(' ( +3.14+j )')", + "complex('1')", + "complex('1+10j')", + "complex(-0.0, 1.0)", + "complex(0, float('inf'))", + "complex('3.14+1J')", + "complex(float('nan'), float('nan'))", + "complex(3.14)", + "complex(real=17, imag=23)", + "complex(1e+200, 1e+200)", + "complex('+J')", + "complex(0.0, 3.0)", + "complex(1.0, 10)", + "complex(-0.0, 2.0)", + "complex('J')", + "complex(' ( +3.14-J )')", + "complex(float('inf'), 1)", + "complex(repr(1 - 6.0j))", + "complex(-0.0, -0.0)", + "complex('1e-500')", + "complex(real=1 + 2.0j, imag=3 + 4.0j)", + "complex(1, 10.0)", + "complex(1, float('nan'))", + "complex(0.0, 3.14)", + "complex(0.0, 3.14j)", + "complex(1.0, -0.0)" + ] + }, + { + "name": "builtins.BaseException", + "instances": [ + "BaseException()" + ] + }, + { + "name": "types.NoneType", + "instances": [ + "None" + ] + }, + { + "name": "builtins.bytearray", + "instances": [ + "bytearray(b'a')", + "bytearray(100)", + "bytearray(b'\\x00' * 100)", + "bytearray(range(1, 10))", + "bytearray(b'\\x07\\x7f\\x7f')", + "bytearray(b'mutable')", + "bytearray([1, 2])", + "bytearray(range(256))", + "bytearray(b'hell')", + "bytearray([5, 6, 7, 8, 9])", + "bytearray(b'memoryview')", + "bytearray(b'a:b::c:::d')", + "bytearray(b'b')", + "bytearray(b'cd')", + "bytearray(b'world')", + "bytearray(b'[emoryvie]')", + "bytearray(b'x' * 5)", + "bytearray([0, 1, 254, 255])", + "bytearray(b'*$')", + "bytearray(b'abc\\xe9\\x00')", + "bytearray(b'a\\xffb')", + "bytearray(range(100))", + "bytearray(b'[abracadabra]')", + "bytearray([0, 1, 2, 100, 101, 7, 8, 9])", + "bytearray(b'Mary')", + "bytearray(b'baz')", + "bytearray(b'\\xff')", + "bytearray(128 * 1024)", + "bytearray([100, 101])", + "bytearray(b'1')", + "bytearray([1, 2, 3])", + "bytearray(b'')", + "bytearray(b'one')", + "bytearray(b'nul:\\x00')", + "bytearray(1024)", + "bytearray(10)", + "bytearray(b'abcdefgh')", + "bytearray(b'123')", + "bytearray(b'\\x80')", + "bytearray(b'01 had a 9')", + "bytearray(2)", + "bytearray(range(10))", + "bytearray(b'a\\x80b')", + "bytearray(b':a:b::c')", + "bytearray(b'\\x00' * 15 + b'\\x01')", + "bytearray(b'hash this!')", + "bytearray(b'bar')", + "bytearray(b'x' * 4)", + "bytearray([0, 1, 2, 42, 42, 42, 3, 4, 5, 6, 7, 8, 9])", + "bytearray(b'----')", + "bytearray([i for i in range(256)])", + "bytearray(b'bytearray')", + "bytearray(b'spam')", + "bytearray([10, 100, 200])", + "bytearray(b'abcdefghijk')", + "bytearray(b'msssspp')", + "bytearray(b'no error')", + "bytearray(b'YWJj\\n')", + "bytearray([1, 1, 1, 1, 1, 5, 6, 7, 8, 9])", + "bytearray(b'foobaz')", + "bytearray([0])", + "bytearray(b'little lamb---')", + "bytearray(b'abc\\xe9\\x00xxx')", + "bytearray(b'def')", + "bytearray(b'eggs\\n')", + "bytearray(b'foo')", + "bytearray(b'foobar')", + "bytearray(128)", + "bytearray(b'key')", + "bytearray(16)", + "bytearray(b'file.py')", + "bytearray(b'ab')", + "bytearray(b'this is a random bytearray object')", + "bytearray(b'x' * 8)", + "bytearray(b' world\\n\\n\\n')", + "bytearray([1, 100, 200])", + "bytearray([102, 111, 111, 111, 111])", + "bytearray(range(1, 9))", + "bytearray([126, 127, 128, 129])", + "bytearray(5)", + "bytearray([0, 1, 2, 102, 111, 111])", + "bytearray(b'\\x00python\\x00test\\x00')", + "bytearray(9)", + "bytearray(b'abcde')", + "bytearray(b'x')", + "bytearray(b'0123456789')", + "bytearray([102, 111, 111, 102, 111, 111])", + "bytearray(2 ** 16)", + "bytearray(b'python')", + "bytearray(8192)", + "bytearray(list(range(8)) + list(range(256)))", + "bytearray(b'\\xff\\x00\\x00')", + "bytearray(b'hello1')", + "bytearray(range(16))", + "bytearray(b'xyz')", + "bytearray(b'\\xaaU\\xaaU')", + "bytearray(b'Z')", + "bytearray(8)", + "bytearray([1, 1, 1, 1, 1])", + "bytearray([0, 1, 2, 3, 4])", + "bytearray(b'ghi')", + "bytearray(b'[ytearra]')", + "bytearray(b'abc')", + "bytearray(b'this is a test')", + "bytearray(b'xxx')", + "bytearray()", + "bytearray(b'abcdefghijklmnopqrstuvwxyz')", + "bytearray(b'my dog has fleas')", + "bytearray([1, 100, 3])", + "bytearray(b'g\\xfcrk')", + "bytearray(b'hello world')", + "bytearray([26, 43, 48])", + "bytearray([1, 2, 3, 4, 6, 7, 8])", + "bytearray(b'0102abcdef')", + "bytearray(1)", + "bytearray(b'--------------')", + "bytearray(20)", + "bytearray(b'hello')" + ] + }, + { + "name": "builtins.bytes", + "instances": [ + "bytes(b'ab')", + "bytes(b'def')", + "bytes(b'abc')", + "bytes([126, 128, 129])", + "bytes([126, 128])", + "bytes(b'Hello world\\n\\x80\\x81\\xfe\\xff')", + "bytes(range(255))", + "bytes(3)", + "bytes(2)" + ] + }, + { + "name": "builtins.dict", + "instances": [ + "dict()" + ] + }, + { + "name": "builtins.frozenset", + "instances": [ + "frozenset()" + ] + }, + { + "name": "builtins.list", + "instances": [ + "list()" + ] + }, + { + "name": "builtins.memoryview", + "instances": [ + "memoryview(b'a')", + "memoryview(b'1234')", + "memoryview(b'ax = 123')", + "memoryview(b'$23$')", + "memoryview(bytes(range(256)))", + "memoryview(b'hash this!')", + "memoryview(b'12.3')", + "memoryview(b'bytes')", + "memoryview(b'a:b::c:::d')", + "memoryview(b'ab')", + "memoryview(b'123')", + "memoryview(b'ac')", + "memoryview(b'YWJj\\n')", + "memoryview(b'')", + "memoryview(b'*$')", + "memoryview(b'spam\\n')", + "memoryview(b'\\xff\\x00\\x00')", + "memoryview(b' ')", + "memoryview(b'abc')", + "memoryview(b'12.3A')", + "memoryview(b':a:b::c')", + "memoryview(b'foo')", + "memoryview(b'\\x124Vx')", + "memoryview(b'[abracadabra]')", + "memoryview(b'123 ')", + "memoryview(b'spam')", + "memoryview(b'text')", + "memoryview(b'memoryview')", + "memoryview(b'123\\x00')", + "memoryview(b'12.3\\x00')", + "memoryview(b'character buffers are decoded to unicode')", + "memoryview(b'xyz')", + "memoryview(b'12.3 ')", + "memoryview(b'cd')", + "memoryview(b'baz')", + "memoryview(b'0102abcdef')", + "memoryview(b'123A')", + "memoryview(b'\\x07\\x7f\\x7f')", + "memoryview(b'file.py')", + "memoryview(b'\\x1a+0')", + "memoryview(b'12.34')" + ] + }, + { + "name": "builtins.object", + "instances": [ + "object()" + ] + }, + { + "name": "builtins.set", + "instances": [ + "set()" + ] + }, + { + "name": "builtins.tuple", + "instances": [ + "tuple()" + ] + }, + { + "name": "builtins.slice", + "instances": [ + "slice(0, 2, 1)", + "slice(2)", + "slice(1, 3)", + "slice(4)", + "slice(0, 10)", + "slice(0, 1, 1)", + "slice(0, 1, 2)", + "slice(0, 2)", + "slice(1, 1)", + "slice(0)", + "slice(0, 1, 2)", + "slice(0, 2, 1)", + "slice(3, 5, 1)", + "slice(0, 1, 1)", + "slice(0, 1, 0)", + "slice(0, 8, 1)", + "slice(0, 2, 0)", + "slice(1, 18, 2)", + "slice(1)", + "slice(0, 10, 1)", + "slice(None, 10, -1)", + "slice(None, -10)", + "slice(None, -11, -1)", + "slice(None, 9)", + "slice(100, -100, -1)", + "slice(None, 10)", + "slice(None)", + "slice(-100, 100)", + "slice(None, None, -1)", + "slice(None, -9)", + "slice(None, 9, -1)", + "slice(0.0, 10, 1)", + "slice(0, 10, 0)", + "slice(10, 20, 3)", + "slice(0, 10, 1.0)", + "slice(1, 2, 4)", + "slice(1, 2)", + "slice(None, None, -2)", + "slice(-100, 100, 2)", + "slice(0, 10.0, 1)", + "slice(3, None, -2)", + "slice(1, None, 2)", + "slice(None, -10, -1)", + "slice(None, 11)", + "slice(1, 2, 3)", + "slice(None, -12, -1)", + "slice(5)", + "slice(None, 8, -1)", + "slice(None, -11)", + "slice(None, None, 2)", + "slice(0, 10, 2)", + "slice(2, 3)", + "slice(0, 10)", + "slice(0, 1, 5)", + "slice(0, 10, 0)", + "slice(2, 10, 3)", + "slice(0, 2)", + "slice(1, 3)", + "slice(1, 2)", + "slice(2, 2)", + "slice(0, 1)", + "slice(0, 0)", + "slice(2, 1)", + "slice(2000, 1000)", + "slice(0, 1000)", + "slice(0, 3)", + "slice(1000, 1000)", + "slice(2, 4)", + "slice(1, 2)", + "slice(1, 2, 3)", + "slice(0, 1)", + "slice(0, 0)", + "slice(2, 3)", + "slice(None, 42)", + "slice(None, 24, None)", + "slice(2, 1024, 10)", + "slice(None, 42, None)", + "slice(0, 2)", + "slice(1, 2)", + "slice(0, 1)", + "slice(3, 5)", + "slice(0, 10, 0)", + "slice(0, 3)", + "slice(1, 10, 2)", + "slice(2, 2, 2)", + "slice(1, 1, 1)" + ] + }, + { + "name": "builtins.type", + "instances": [ + "type(len)", + "type('123')", + "type(1.0)", + "type({})", + "type('foo', (), {})", + "type('A', (), {'__qualname__': 'B.C'})", + "type(lambda x: x)", + "type((True).real)", + "type(list.append)", + "type('blah', (), {})", + "type(())", + "type('A', (), {})", + "type(list)", + "type({}.items())", + "type({}.values())", + "type('NewClass', (object,), {})", + "type(list.__add__)", + "type(classmethod(lambda c: None))", + "type(range(0))", + "type(None)", + "type(iter(range(0)))", + "type('C', (object,), {'__hash__': None})", + "type(complex('1' * 500))", + "type(staticmethod(lambda : None))", + "type(iter(range(1 << 1000)))", + "type('C', (), {})", + "type({}.keys())" + ] + }, + { + "name": "datetime.date", + "instances": [ + "datetime.date(1, 1, 1)", + "datetime.date(1995, 4, 12)", + "datetime.date(2011, 1, 1)", + "datetime.date(2002, 3, 4)", + "datetime.date(1993, 8, 26)", + "datetime.date(2000, 1, 2)", + "datetime.date(1970, 1, 1)" + ] + }, + { + "name": "datetime.datetime", + "instances": [ + "datetime.datetime(2011, 1, 1)", + "datetime.datetime(1970, 1, 1)", + "datetime.datetime(2015, 4, 5, 1, 45)", + "datetime.datetime(1, 1, 1)", + "datetime.datetime(2014, 11, 2, 1, 30)", + "datetime.datetime(1, 2, 3, 4, 5, 6, 7)", + "datetime.datetime(2002, 4, 7, 2)", + "datetime.datetime(1, 1, 1, fold=1)", + "datetime.datetime(2010, 1, 1)", + "datetime.datetime(2011, 1, 1, 12, 30)", + "datetime.datetime(1993, 8, 26, 22, 12, 55, 99999)", + "datetime.datetime(1, 4, 1, 2)", + "datetime.datetime(1995, 4, 12)", + "datetime.datetime(1, 10, 25, 1)", + "datetime.datetime(10, 10, 10, 10, 10, 10, 10)", + "datetime.datetime(2002, 10, 27, 1)" + ] + }, + { + "name": "datetime.time", + "instances": [ + "datetime.time(fold=1)", + "datetime.time()", + "datetime.time(0, fold=1)", + "datetime.time(22, 12, 55, 99999)", + "datetime.time(0)", + "datetime.time(microsecond=40)", + "datetime.time(18, 45, 3, 1234)", + "datetime.time(12, 0)", + "datetime.time(12, 30)" + ] + }, + { + "name": "datetime.timedelta", + "instances": [ + "datetime.timedelta(days=100, weeks=-7, hours=-24 * (100 - 49), minutes=-3, seconds=12, microseconds=(3 * 60 - 12) * 1000000)", + "datetime.timedelta(hours=24)", + "datetime.timedelta(hours=23, minutes=59)", + "datetime.timedelta(0, 4000, 1)", + "datetime.timedelta(days=999999999, hours=23, minutes=59, seconds=59, microseconds=999999)", + "datetime.timedelta(minutes=1440)", + "datetime.timedelta(microseconds=1)", + "datetime.timedelta(minutes=24)", + "datetime.timedelta(minutes=60)", + "datetime.timedelta(weeks=13)", + "datetime.timedelta(minutes=2, seconds=1, microseconds=3)", + "datetime.timedelta(26, 55, 99999)", + "datetime.timedelta(seconds=0.5)", + "datetime.timedelta(minutes=-200)", + "datetime.timedelta(seconds=30)", + "datetime.timedelta(days=-999999999)", + "datetime.timedelta(minutes=-2)", + "datetime.timedelta(minutes=2 * 1439)", + "datetime.timedelta(hours=12, minutes=32, seconds=30)", + "datetime.timedelta(hours=-5)", + "datetime.timedelta(hours=5)", + "datetime.timedelta(42)", + "datetime.timedelta(minutes=23)", + "datetime.timedelta(minutes=3)", + "datetime.timedelta(microseconds=-1)", + "datetime.timedelta(0, 0, 1000)", + "datetime.timedelta(minutes=1)", + "datetime.timedelta(days=365)", + "datetime.timedelta(minutes=0)", + "datetime.timedelta(microseconds=-81)", + "datetime.timedelta(hours=9.5)", + "datetime.timedelta(minutes=2, seconds=30)", + "datetime.timedelta(hours=-4)", + "datetime.timedelta(minutes=-300)", + "datetime.timedelta(days=1, seconds=2, microseconds=3)", + "datetime.timedelta(hours=2)", + "datetime.timedelta(0)" + ] + }, + { + "name": "_decimal.Decimal", + "instances": [ + "_decimal.Decimal('22.2')", + "_decimal.Decimal('1.234e7')", + "_decimal.Decimal('sNaN')", + "_decimal.Decimal(3)", + "_decimal.Decimal('45.34')", + "_decimal.Decimal('580')", + "_decimal.Decimal((0, (0,), 0))", + "_decimal.Decimal('3.4e200')", + "_decimal.Decimal('1e2')", + "_decimal.Decimal('2.59')", + "_decimal.Decimal((1, (0, 0, 0), 37))", + "_decimal.Decimal(10000)", + "_decimal.Decimal('10e99999')", + "_decimal.Decimal(7.5)", + "_decimal.Decimal('1.1')", + "_decimal.Decimal(10 ** (19 * 24))", + "_decimal.Decimal('-inf')", + "_decimal.Decimal(' 3.45679 ')", + "_decimal.Decimal('1.0e-20')", + "_decimal.Decimal('-0.8')", + "_decimal.Decimal('1652.9E100')", + "_decimal.Decimal('-10')", + "_decimal.Decimal('0E10')", + "_decimal.Decimal('2.54')", + "_decimal.Decimal('15.32')", + "_decimal.Decimal('-0')", + "_decimal.Decimal('0.00390625')", + "_decimal.Decimal(5)", + "_decimal.Decimal((1, (0, 0, 0), 'N'))", + "_decimal.Decimal('-3.141590000')", + "_decimal.Decimal('0')", + "_decimal.Decimal(45)", + "_decimal.Decimal('1234e9999')", + "_decimal.Decimal(2)", + "_decimal.Decimal('1.634E100')", + "_decimal.Decimal('4.125')", + "_decimal.Decimal('4.2084')", + "_decimal.Decimal('56531E100')", + "_decimal.Decimal((1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25))", + "_decimal.Decimal('9.99')", + "_decimal.Decimal('45')", + "_decimal.Decimal('100.0')", + "_decimal.Decimal('7')", + "_decimal.Decimal('1.01')", + "_decimal.Decimal('111')", + "_decimal.Decimal(2 ** 16)", + "_decimal.Decimal('0.0012885819')", + "_decimal.Decimal('1')", + "_decimal.Decimal(-12)", + "_decimal.Decimal('1.12345')", + "_decimal.Decimal('-0.5')", + "_decimal.Decimal((0, (4, 5, 3, 4), -2))", + "_decimal.Decimal('-0.4')", + "_decimal.Decimal('-33.3')", + "_decimal.Decimal(2 ** 578)", + "_decimal.Decimal('1.00000001e-20')", + "_decimal.Decimal('10001111111')", + "_decimal.Decimal([0, [0], 0])", + "_decimal.Decimal('INF')", + "_decimal.Decimal(2 ** 64 + 2 ** 32 - 1)", + "_decimal.Decimal('4.9712')", + "_decimal.Decimal('8.392')", + "_decimal.Decimal('1e-9999')", + "_decimal.Decimal('9.123')", + "_decimal.Decimal('1e99')", + "_decimal.Decimal('1.47e5')", + "_decimal.Decimal(23)", + "_decimal.Decimal('3.0')", + "_decimal.Decimal('152587890625')", + "_decimal.Decimal('3.1234')", + "_decimal.Decimal(+45)", + "_decimal.Decimal(float('nan'))", + "_decimal.Decimal('32')", + "_decimal.Decimal('snan123')", + "_decimal.Decimal(10101)", + "_decimal.Decimal('28.5')", + "_decimal.Decimal('0.00')", + "_decimal.Decimal('11.68')", + "_decimal.Decimal('99999999999999999999999999.9')", + "_decimal.Decimal('1_0_0_0')", + "_decimal.Decimal('5')", + "_decimal.Decimal('1.00000001e-100')", + "_decimal.Decimal('0.28')", + "_decimal.Decimal('NaN')", + "_decimal.Decimal((0, (), 'F'))", + "_decimal.Decimal('-NaN')", + "_decimal.Decimal('9.87654321')", + "_decimal.Decimal('10912837129')", + "_decimal.Decimal('1.00000001')", + "_decimal.Decimal('2E+1')", + "_decimal.Decimal('-1')", + "_decimal.Decimal('Nan891287828')", + "_decimal.Decimal('-11.1')", + "_decimal.Decimal((1, (), 37))", + "_decimal.Decimal('1e1')", + "_decimal.Decimal('NAN')", + "_decimal.Decimal('9.8765e-12')", + "_decimal.Decimal(99)", + "_decimal.Decimal('625')", + "_decimal.Decimal(200)", + "_decimal.Decimal(123)", + "_decimal.Decimal('1.00')", + "_decimal.Decimal('81.3971')", + "_decimal.Decimal('123456789.1')", + "_decimal.Decimal('0.372')", + "_decimal.Decimal('1.23')", + "_decimal.Decimal(1234)", + "_decimal.Decimal('-21.1')", + "_decimal.Decimal('10901935')", + "_decimal.Decimal('-0E12')", + "_decimal.Decimal('Inf')", + "_decimal.Decimal((0, (0,), 'F'))", + "_decimal.Decimal('-0.6')", + "_decimal.Decimal('9e2')", + "_decimal.Decimal('35.719')", + "_decimal.Decimal('390625')", + "_decimal.Decimal(10 ** (19 * 25))", + "_decimal.Decimal('-2.5')", + "_decimal.Decimal('3.571')", + "_decimal.Decimal('1e797')", + "_decimal.Decimal('1e-425000000')", + "_decimal.Decimal('188.83E100')", + "_decimal.Decimal('0.001')", + "_decimal.Decimal((1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25))", + "_decimal.Decimal('100E-425000010')", + "_decimal.Decimal('1e100000')", + "_decimal.Decimal('3.5e-2')", + "_decimal.Decimal('100000000000000000000000000')", + "_decimal.Decimal('-5')", + "_decimal.Decimal(11)", + "_decimal.Decimal('1.0e20')", + "_decimal.Decimal('1e4')", + "_decimal.Decimal(1001)", + "_decimal.Decimal('-4.34913534E-17')", + "_decimal.Decimal('-23.00000')", + "_decimal.Decimal('-25e55')", + "_decimal.Decimal('456789')", + "_decimal.Decimal('10')", + "_decimal.Decimal('999.9')", + "_decimal.Decimal([1, (4, 3, 4, 9, 1, 3, 5, 3, 4), -25])", + "_decimal.Decimal(0)", + "_decimal.Decimal('-0.0625')", + "_decimal.Decimal('9.99e-5')", + "_decimal.Decimal('256e7')", + "_decimal.Decimal('1.3E4 \\n')", + "_decimal.Decimal()", + "_decimal.Decimal('10.0')", + "_decimal.Decimal('3.1415926')", + "_decimal.Decimal(0.1)", + "_decimal.Decimal('0.25')", + "_decimal.Decimal('9.8182731e181273')", + "_decimal.Decimal('16.1')", + "_decimal.Decimal('nan')", + "_decimal.Decimal('-3.217160342717258261933904529E-7')", + "_decimal.Decimal('1e9999')", + "_decimal.Decimal('0.05')", + "_decimal.Decimal('25')", + "_decimal.Decimal(100)", + "_decimal.Decimal(-2)", + "_decimal.Decimal('NaN12345')", + "_decimal.Decimal('1e-99')", + "_decimal.Decimal('0.' + '9' * 30)", + "_decimal.Decimal(1221 ** 1271)", + "_decimal.Decimal('32.9714')", + "_decimal.Decimal((1, (0, 2, 7, 1), 'F'))", + "_decimal.Decimal((1, (4, 5), 0))", + "_decimal.Decimal(50)", + "_decimal.Decimal([1, [4, 3, 4, 9, 1, 3, 5, 3, 4], -25])", + "_decimal.Decimal('45e2')", + "_decimal.Decimal('2.234e2000')", + "_decimal.Decimal(1000)", + "_decimal.Decimal('7.33')", + "_decimal.Decimal('5e3')", + "_decimal.Decimal('-0.0')", + "_decimal.Decimal('1.0e-100')", + "_decimal.Decimal('5.5')", + "_decimal.Decimal('-75')", + "_decimal.Decimal('1.2')", + "_decimal.Decimal('-0.625')", + "_decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), 'n'))", + "_decimal.Decimal('33.3')", + "_decimal.Decimal(9)", + "_decimal.Decimal(4)", + "_decimal.Decimal('1230E100')", + "_decimal.Decimal('.000e20')", + "_decimal.Decimal((0, (0, 0, 4, 0, 5, 3, 4), -2))", + "_decimal.Decimal(67)", + "_decimal.Decimal('sNAN')", + "_decimal.Decimal(float('-inf'))", + "_decimal.Decimal(123456789000)", + "_decimal.Decimal('NaN123')", + "_decimal.Decimal(True)", + "_decimal.Decimal('5e-3')", + "_decimal.Decimal('-6.1')", + "_decimal.Decimal('-25')", + "_decimal.Decimal('2')", + "_decimal.Decimal('-1.25')", + "_decimal.Decimal('0.1')", + "_decimal.Decimal('1.0')", + "_decimal.Decimal('90.697E100')", + "_decimal.Decimal(152587890625)", + "_decimal.Decimal(10 ** (9 * 24))", + "_decimal.Decimal('12.7')", + "_decimal.Decimal('66')", + "_decimal.Decimal(-100)", + "_decimal.Decimal('.1')", + "_decimal.Decimal('7.34')", + "_decimal.Decimal(float('inf'))", + "_decimal.Decimal('7.335')", + "_decimal.Decimal('1.2345')", + "_decimal.Decimal('0.2')", + "_decimal.Decimal((0, (4, 5, 3, 4), 'F'))", + "_decimal.Decimal('Infinity')", + "_decimal.Decimal('0.871831e800')", + "_decimal.Decimal('1.00000001e20')", + "_decimal.Decimal('3.1415')", + "_decimal.Decimal('-Inf')", + "_decimal.Decimal('-nan')", + "_decimal.Decimal(456)", + "_decimal.Decimal('194')", + "_decimal.Decimal('0.025')", + "_decimal.Decimal('snan')", + "_decimal.Decimal(567)", + "_decimal.Decimal('9.8765e12')", + "_decimal.Decimal('3.1')", + "_decimal.Decimal('-1.5')", + "_decimal.Decimal('inf')", + "_decimal.Decimal('1.3')", + "_decimal.Decimal('-4.5678E50')", + "_decimal.Decimal(5 ** 2659)", + "_decimal.Decimal('33e+33')", + "_decimal.Decimal('1e-100000')", + "_decimal.Decimal(float('-0.0'))", + "_decimal.Decimal('1.23456789')", + "_decimal.Decimal('1e-10')", + "_decimal.Decimal(12)", + "_decimal.Decimal('nan123')", + "_decimal.Decimal('0.0')", + "_decimal.Decimal('-0.000')", + "_decimal.Decimal('152587890625e7')", + "_decimal.Decimal('-16.1')", + "_decimal.Decimal('0.333333333333333333')", + "_decimal.Decimal(-1)", + "_decimal.Decimal('43.24')", + "_decimal.Decimal('9.9')", + "_decimal.Decimal('3.1416')", + "_decimal.Decimal('2.1')", + "_decimal.Decimal('16807')", + "_decimal.Decimal('62.4802')", + "_decimal.Decimal('-15')", + "_decimal.Decimal(500000123)", + "_decimal.Decimal('-38.3')", + "_decimal.Decimal('0.333333333333333333333333')", + "_decimal.Decimal('1e425000000')", + "_decimal.Decimal('1.5')", + "_decimal.Decimal(768)", + "_decimal.Decimal(10 ** (9 * 25))", + "_decimal.Decimal((1, (), 'n'))", + "_decimal.Decimal('11.1')", + "_decimal.Decimal('0.01')", + "_decimal.Decimal(-45)", + "_decimal.Decimal('9.99e10')", + "_decimal.Decimal('1e-3')", + "_decimal.Decimal('100000000.123')", + "_decimal.Decimal('0.5')", + "_decimal.Decimal('3')", + "_decimal.Decimal(1)", + "_decimal.Decimal(10)", + "_decimal.Decimal('20')", + "_decimal.Decimal('0.1234')", + "_decimal.Decimal('-Infinity')", + "_decimal.Decimal('.01')", + "_decimal.Decimal('23.42')", + "_decimal.Decimal(' -7.89')", + "_decimal.Decimal(False)", + "_decimal.Decimal('1_3.3e4_0')", + "_decimal.Decimal('2.234e-2000')", + "_decimal.Decimal('-1E+1')", + "_decimal.Decimal('1.50001')", + "_decimal.Decimal('8.71E+799')", + "_decimal.Decimal('20.686')" + ] + } +] diff --git a/utbot-python/src/main/resources/python_tree_serializer.py b/utbot-python/src/main/resources/python_tree_serializer.py new file mode 100644 index 0000000000..557cb56954 --- /dev/null +++ b/utbot-python/src/main/resources/python_tree_serializer.py @@ -0,0 +1,206 @@ +import copy +import pickle +import types +from itertools import zip_longest +import copyreg +import importlib + + +class _PythonTreeSerializer: + class MemoryObj: + def __init__(self, json): + self.json = json + self.deserialized_obj = None + self.comparable = False + self.is_draft = True + + def __init__(self): + self.memory = {} + + def memory_view(self): + return ' | '.join(f'{id_}: {obj.deserialized_obj}' for id_, obj in self.memory.items()) + + @staticmethod + def get_type(py_object): + if py_object is None: + return 'types.NoneType' + module = type(py_object).__module__ + return '{module}.{name}'.format( + module=module, + name=type(py_object).__name__, + ) + + @staticmethod + def get_type_name(type_): + if type_ is None: + return 'types.NoneType' + return '{module}.{name}'.format( + module=type_.__module__, + name=type_.__name__, + ) + + @staticmethod + def has_reduce(py_object) -> bool: + if getattr(py_object, '__reduce__', None) is None: + return False + else: + try: + py_object.__reduce__() + return True + except TypeError: + return False + + def save_to_memory(self, id_, py_json, deserialized_obj): + mem_obj = _PythonTreeSerializer.MemoryObj(py_json) + mem_obj.deserialized_obj = deserialized_obj + self.memory[id_] = mem_obj + return mem_obj + + def get_reduce(self, py_object): + id_ = id(py_object) + + py_object_reduce = py_object.__reduce__() + reduce_value = [ + default if obj is None else obj + for obj, default in zip_longest( + py_object_reduce, + [None, [], {}, [], []], + fillvalue=None + ) + ] + + constructor = _PythonTreeSerializer.get_type_name(reduce_value[0]) + args, deserialized_args = _PythonTreeSerializer.unzip_list([ + self.serialize(arg) + for arg in reduce_value[1] + ]) + json_obj = { + 'id': id_, + 'type': _PythonTreeSerializer.get_type(py_object), + 'constructor': constructor, + 'args': args, + 'state': [], + 'listitems': [], + 'dictitems': [], + } + deserialized_obj = reduce_value[0](*deserialized_args) + memory_obj = self.save_to_memory(id_, json_obj, deserialized_obj) + + state, deserialized_state = self.unzip_dict([ + (attr, self.serialize(value)) + for attr, value in reduce_value[2].items() + ], skip_first=True) + listitems, deserialized_listitems = self.unzip_list([ + self.serialize(item) + for item in reduce_value[3] + ]) + dictitems, deserialized_dictitems = self.unzip_dict([ + (self.serialize(key), self.serialize(value)) + for key, value in reduce_value[4] + ]) + + memory_obj.json['state'] = state + memory_obj.json['listitems'] = listitems + memory_obj.json['dictitems'] = dictitems + + for key, value in deserialized_state.items(): + setattr(deserialized_obj, key, value) + for item in deserialized_listitems: + deserialized_obj.append(item) + for key, value in deserialized_dictitems.items(): + deserialized_obj[key] = value + + memory_obj.deserialized_obj = deserialized_obj + memory_obj.is_draft = False + + return id_, deserialized_obj + + def serialize(self, py_object): + type_ = _PythonTreeSerializer.get_type(py_object) + id_ = id(py_object) + skip_comparable = False + comparable = True + + if id_ in self.memory: + value = id_ + strategy = 'memory' + skip_comparable = True + comparable = False + deserialized_obj = self.memory[id_].deserialized_obj + if not self.memory[id_].is_draft: + self.memory[id_].comparable = py_object == deserialized_obj + skip_comparable = False + elif isinstance(py_object, type): + value = _PythonTreeSerializer.get_type_name(py_object) + strategy = 'repr' + deserialized_obj = py_object + elif any(type(py_object) == t for t in (list, set, tuple)): + elements = [ + self.serialize(element) for element in py_object + ] + value, deserialized_obj = _PythonTreeSerializer.unzip_list(elements, type(py_object)) + comparable = all([element['comparable'] for element in value]) + strategy = 'generic' + elif type(py_object) == dict: + elements = [ + [self.serialize(key), self.serialize(value)] + for key, value in py_object.items() + ] + value, deserialized_obj = _PythonTreeSerializer.unzip_dict(elements) + comparable = all([element[1]['comparable'] for element in value]) + strategy = 'generic' + elif _PythonTreeSerializer.has_reduce(py_object): + value, deserialized_obj = self.get_reduce(py_object) + strategy = 'memory' + else: + value = repr(py_object) + try: + deserialized_obj = pickle.loads(pickle.dumps(py_object)) + except Exception: + deserialized_obj = py_object + skip_comparable = True + comparable = False + strategy = 'repr' + + if not skip_comparable: + try: + comparable = comparable and (py_object == deserialized_obj) + except Exception: + comparable = False + + return { + 'type': type_, + 'value': value, + 'strategy': strategy, + 'comparable': comparable, + }, deserialized_obj + + @staticmethod + def unzip_list(elements, cast_second=list): + if len(elements) == 0: + first, second = [], [] + else: + first, second = list(zip(*elements)) + return first, cast_second(second) + + @staticmethod + def unzip_dict(elements, cast_second=dict, skip_first=False): + if len(elements) == 0: + first, second = [], [] + else: + if skip_first: + first = [[element[0], element[1][0]] for element in elements] + second = [[element[0], element[1][1]] for element in elements] + else: + first = [[element[0][0], element[1][0]] for element in elements] + second = [[element[0][1], element[1][1]] for element in elements] + return first, cast_second(second) + + def dumps(self, obj): + return { + 'json': self.serialize(obj)[0], + 'memory': { + key: value.json + for key, value in self.memory.items() + } + } diff --git a/utbot-python/src/main/resources/requirements.txt b/utbot-python/src/main/resources/requirements.txt new file mode 100644 index 0000000000..60cf8c7abe --- /dev/null +++ b/utbot-python/src/main/resources/requirements.txt @@ -0,0 +1,4 @@ +mypy==0.971 +astor +typeshed-client +coverage \ No newline at end of file diff --git a/utbot-python/src/main/resources/typeshed_stub.py b/utbot-python/src/main/resources/typeshed_stub.py new file mode 100644 index 0000000000..7d77a113a2 --- /dev/null +++ b/utbot-python/src/main/resources/typeshed_stub.py @@ -0,0 +1,322 @@ +import ast +import importlib +import json +import sys +import os + +import mypy.fastparse + +from contextlib import contextmanager +from collections import defaultdict + +import astor +from typeshed_client import get_stub_names, get_search_context, OverloadedName + + +def normalize_annotation(annotation, module_of_annotation): + def walk_mypy_type(mypy_type): + try: + prefix = f'{module_of_annotation}.' if len(module_of_annotation) > 0 else '' + + if mypy_type.name[:len(prefix)] == prefix: + name = mypy_type.name[len(prefix):] + else: + name = mypy_type.name + + if eval(name) is None: + result = "types.NoneType" + else: + modname = eval(name).__module__ + result = f'{modname}.{name}' + + except Exception as e: + result = 'typing.Any' + + if hasattr(mypy_type, 'args') and len(mypy_type.args) != 0: + arg_strs = [ + walk_mypy_type(arg) + for arg in mypy_type.args + ] + result += f"[{', '.join(arg_strs)}]" + + return result + + mod = importlib.import_module(module_of_annotation) + + for name in dir(mod): + globals()[name] = getattr(mod, name) + + mypy_type_ = mypy.fastparse.parse_type_string(annotation, annotation, -1, -1) + return walk_mypy_type(mypy_type_) + + +class AstClassEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.ClassDef): + json_dump = { + 'className': o.name, + 'methods': [], + 'fields': [], + } + + def _function_statements_handler(_statement): + if isinstance(_statement, ast.FunctionDef): + method = AstFunctionDefEncoder().default(_statement) + is_property = method['is_property'] + del method['is_property'] + if is_property: + del method['args'] + del method['kwonlyargs'] + + method['annotation'] = method['returns'] + del method['returns'] + + json_dump['fields'].append(method) + else: + json_dump['methods'].append(method) + if isinstance(_statement, ast.AnnAssign): + field = AstAnnAssignEncoder().default(_statement) + json_dump['fields'].append(field) + + for statement in o.body: + _function_statements_handler(statement) + + return json_dump + return json.JSONEncoder.default(self, o) + + +class AstAnnAssignEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.AnnAssign): + json_dump = { + 'name': '...' if isinstance(o.target, type(Ellipsis)) else o.target.id, + 'annotation': transform_annotation(o.annotation), + } + return json_dump + return json.JSONEncoder.default(self, o) + + +def find_init_method(function_ast): + for statement in function_ast.body: + if isinstance(statement, ast.FunctionDef) and statement.name == '__init__': + return statement + return None + + +class AstFunctionDefEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, (ast.FunctionDef, ast.AsyncFunctionDef)): + json_dump = { + 'name': o.name, + 'returns': transform_annotation(o.returns), + 'args': [ + AstArgEncoder().default(arg) + for arg in o.args.args + ], + 'kwonlyargs': [ + AstArgEncoder().default(arg) + for arg in o.args.kwonlyargs + ], + 'is_property': function_is_property(o), + } + return json_dump + + +def function_is_property(function): + return bool(any([ + 'property' == astor.code_gen.to_source(decorator).strip() + for decorator in function.decorator_list + ])) + + +class AstArgEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.arg): + json_dump = { + 'arg': o.arg, + 'annotation': transform_annotation(o.annotation) + } + return json_dump + + +class AstConstantEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, ast.Constant): + json_dump = '...' if isinstance(o.value, type(Ellipsis)) else o.value + + return json_dump + if isinstance(o, type(Ellipsis)): + return '...' + if o is None: + return None + + +def transform_annotation(annotation): + return '' if annotation is None else astor.code_gen.to_source(annotation).strip() + + +def recursive_normalize_annotations(json_data, module_name): + if 'annotation' in json_data: + json_data['annotation'] = normalize_annotation( + annotation=json_data['annotation'], + module_of_annotation=module_name + ) + elif 'returns' in json_data: + json_data['returns'] = normalize_annotation( + annotation=json_data['returns'], + module_of_annotation=module_name + ) + json_data['args'] = [ + recursive_normalize_annotations(arg, module_name) + for arg in json_data['args'] + ] + json_data['kwonlyargs'] = [ + recursive_normalize_annotations(arg, module_name) + for arg in json_data['kwonlyargs'] + ] + elif 'className' in json_data: + for key, value in json_data.items(): + if key in {'methods', 'fields'}: + json_data[key] = [ + recursive_normalize_annotations(elem, module_name) + for elem in value + ] + else: + for key, value in json_data.items(): + json_data[key] = [ + recursive_normalize_annotations(elem, module_name) + for elem in value + ] + + return json_data + + +class StubFileCollector: + def __init__(self, python_version): + self.methods_dataset = defaultdict(list) + self.fields_dataset = defaultdict(list) + self.functions_dataset = defaultdict(list) + self.classes_dataset = [] + self.assigns_dataset = defaultdict(list) + self.ann_assigns_dataset = defaultdict(list) + self.python_version = python_version + self.visited_modules = [] + + def create_module_table(self, module_name): + self.visited_modules.append(module_name) + + stub = get_stub_names( + module_name, + search_context=get_search_context(version=self.python_version) + ) + + def _ast_handler(ast_): + if isinstance(ast_, OverloadedName): + for definition in ast_.definitions: + _ast_handler(definition) + else: + if isinstance(ast_, ast.ClassDef): + json_data = AstClassEncoder().default(ast_) + recursive_normalize_annotations(json_data, module_name) + + if not ast_.name.startswith('_'): + class_name = f'{module_name}.{ast_.name}' + json_data['className'] = class_name + self.classes_dataset.append(json_data) + + for method in json_data['methods']: + method['className'] = class_name + self.methods_dataset[method['name']].append(method) + + for field in json_data['fields']: + field['className'] = class_name + self.fields_dataset[field['name']].append(field) + + elif isinstance(ast_, (ast.FunctionDef, ast.AsyncFunctionDef)): + json_data = AstFunctionDefEncoder().default(ast_) + recursive_normalize_annotations(json_data, module_name) + + function_name = f'{module_name}.{ast_.name}' + json_data['name'] = function_name + json_data['className'] = None + self.functions_dataset[ast_.name].append(json_data) + + else: + pass + + ast_nodes = set() + + if stub is None: + return + + for name, name_info in stub.items(): + ast_nodes.add(name_info.ast.__class__.__name__) + _ast_handler(name_info.ast) + + def save_method_annotations(self): + return json.dumps({ + 'classAnnotations': self.classes_dataset, + 'fieldAnnotations': defaultdict_to_array(self.fields_dataset), + 'functionAnnotations': defaultdict_to_array(self.functions_dataset), + 'methodAnnotations': defaultdict_to_array(self.methods_dataset), + }) + + +def defaultdict_to_array(dataset): + return [ + { + 'name': name, + 'definitions': types, + } + for name, types in dataset.items() + ] + + +def parse_submodule(module_name, collector_): + collector_.create_module_table(module_name) + try: + submodules = [ + f'{module_name}.{submodule}' if module_name != 'builtins' else submodule + for submodule in importlib.import_module(module_name).__dir__() + ] + for submodule in submodules: + if type(eval(submodule)) == 'module' and submodule not in collector_.visited_modules: + parse_submodule(submodule, collector_) + except ModuleNotFoundError: + pass + except ImportError: + pass + except NameError: + pass + except AttributeError: + pass + + +@contextmanager +def suppress_stdout(): + with open(os.devnull, "w") as devnull: + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = devnull + sys.stderr = devnull + try: + yield + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + +def main(): + python_version = sys.version_info + modules = sys.argv[1:] + with suppress_stdout(): + collector = StubFileCollector((python_version.major, python_version.minor)) + for module in modules: + parse_submodule(module, collector) + result = collector.save_method_annotations() + sys.stdout.write(result) + + +if __name__ == '__main__': + main() + sys.exit(0) diff --git a/utbot-python/todo.md b/utbot-python/todo.md new file mode 100644 index 0000000000..0ffe45e242 --- /dev/null +++ b/utbot-python/todo.md @@ -0,0 +1,62 @@ +### Большие задачи +- [ ] Более умная работа с питоновскими аннотациями +- [ ] Переписать угадывание типов +- [ ] Придумать, как обрабатывать декораторы +- [ ] Заменить парсер питоновского кода (чтобы поддерживать новые языковые конструкции) +- [ ] Assert'ы не только для возвращаемого значения + - [ ] сериализация аргументов функции перед тестом и после + - [ ] сериализация класса перед тестом и после теста + - [ ] Assert'ы для переменных из замыкания? (Можно считать, что это аналог static полей из джавы) +- [ ] PythonTree <-> AssembleModel? +- [ ] (совсем на будущее) Можно перенести генерацию временных питоновский скриптов (для запуска функции, для mypy) в общий кодогенератор? +- [ ] Добавить mock-объекты +- [ ] Запуск в изолированном окружении +- [x] Вынести Python-CLI + +### Относительно мелкие задачи +- [x] Все изменения положить в отдельные пакеты, _в том числе перенести Python модели из Api.kt в PythonApi (возможно тоже в другой модуль?)_ + + Кроме Api.kt внутри utbot-framework есть много Python- файлов и when, где есть питоновские ветки + +- [x] Переписать в IdUtils.kt проверку на ClassId.enclosingClass для PythonClassId, по хорошему его нужно убрать, но при этом изменить логику на PythonClassId, так как дефолтная версия использует jClass +- [ ] Обработка исключений в codegen (writeWarningAboutFunction) + + Чтобы сделать нормальную обработку, надо в UtExecutionFailure отказаться от хранения Throwable, заменить его на какую-то абстракцию. + + Сейчас, чтобы можно было хоть как-то упаковывать UtExecutionFailure от питона, я его оборачиваю в Throwable("питоновское исключение"). Понятно, что с таким подходом оттуда еще какую-то информацию (подробное сообщение, stacktrace) не достать. + + В текущей реализации кодогенератор решает, что функция и должна возвращать исключение, и поэтому комментарий сам не добавляет. Чтобы для питона хоть как-то понимать, что в тесте кидается исключение, я добавила в createTestMethod (который теперь находится в PythonCgMethodConstructor.kt) дополнительный комментарий, который бывает только при питоновской кодогенерации. + +- [ ] переписать создание файлов на Psi Documents +- [x] создавать временную директорию в правильном месте +- [ ] обрабатывать аргументы по имени (kwargs), *args, **kwargs +- [ ] заменить klaxon на moshi +- [ ] Более полный сбор конструкторов (учет наследования) <-- решится после переезда на новые аннотации +- [ ] Нормально организовать питоновскую интроспекцию (не создавать каждый раз новый процесс) <-- скорее всего решится после переезда на новые аннотации +- [ ] _учитывать AnnAssign_ +- [ ] учитывать в ArgInfoCollector ситуации вида `arg.field[0]` <-- решится после переписывания угадывания типов +- [x] Понять, когда завершать fuzzing + - Либо timeout, либо кончились модели провайдеров? +- [x] json serializers для результата функции +- [x] обрабатывать вызов функции вида module.func() +- [x] разобраться с `nan` (`nan == nan` -> `False`) +- [x] добавить обработку dataclass'ов +- [x] импорты в codegen +- [x] `sys.path` в codegen +- [x] получение `sys.path` из настроек проекта +- [x] добавить сообщение о mypy ошибках +- [x] обработка возвращаемого значения +- [x] алгоритм assert для списков / словарей +- [x] большие числа при генерации списков??? + + +Проблемы могут быть с: `__getitem__`/`__class_getitem__`, +`__add__/__radd__/__iadd__` + +Комментарии по типам: + +- `datetime.timezone(x: timedelta)` +- `NoneType`? + + +В чем идея PresitentSet и internal переопределение операций +=???? diff --git a/utbot-python/todo_refactoring.md b/utbot-python/todo_refactoring.md new file mode 100644 index 0000000000..b405e561ec --- /dev/null +++ b/utbot-python/todo_refactoring.md @@ -0,0 +1,57 @@ +# Задачи + +- [x] добавление импортов внутри testFramework (нормально делать `import unittest`). У TestClassContext есть поле testClassSuperclass, но оно иницализируется null в двух местах в CgContext, нужно либо менять логику там и добавлять аргументы, либо задавать его, как сейчас, в PythonCgTestClassConstructor: + ```python + # line 69 + with (currentTestClassContext) { + annotations += collectedTestClassAnnotations + superclass = testFramework.testSuperClass + interfaces += collectedTestClassInterfaces + } + ``` + Кроме этого добавил поле testSuperClass в TestFramework (null по умолчанию), которе задается сейчас только для unittest. + +- [x] importIfNeeded для моделей, которые подаются на вход функции + + В джаве никогда такого не возникало, для питона это нормальная ситуация, которую надо обработать. + +- [x] Переделать sys.path в import. + + Возможное решение: разные наследники PythonImport, введение порядка на импортах, установка приоритетов. Часть логики уже написана, но за последний день мы не успели. + +- [x] Конфликты имен переменных с модулями (или еще чем-нибудь) + + Сейчас это делается через изменение existingVariableNames, это раскидано в нескольких местах в файлах PythonCg...kt. -> _уже не раскидано_ + + Можно сделать в CgContext отдельный контейнер для таких ситуаций, и учитывать его в CgNameGenerator. + + Возможная проблема: мы можем сначала создать переменную, а потом импортировать модуль с таким именем. Возможное решение: редактировать Cg элементы уже созданных переменных. + + __Update__: Добавил в PythonCgNameGenerator проверку на совпадение c каким-либо из импортированных модулей. Если для составных питоновских объектов все импорты собираются в allContainingsClassId, то этого достаточно, чтобы сохранить в контекст все импорты перед началом генерации всех тестов. + + Единственный импорт, который добавляется сейчас внутри генерации - copyreg, наверное, его нужно добавлять в allContainingClassIds. __Done__ + +- [ ] Убрать if-ы из Cg... с Python -> (перенес логику из CgMethodConstructor в PythonCgMethodConstructor) + Внутри Cg... единственная python-логика осталась в функции pythonGenerateAsStringWithTestReport в CodeGenerator, остальные перенесены в PythonCg... + + Постепенно убираются ифы и из PythonCg..., но пока есть моменты, которые не убрать на данный момент + + +# Вопросы +* В каком месте создавать python пакет в `utbot-framework`? +* Что делать с python-ветками в разных when? +* Что делать с Domain.kt? Нужно ли убирать оттуда питоновские тестовые фреймворки + + +1. Запустить с разными Pycharm + +| IDE | Результат | +|-----|-------------| +| IC | Работает | +| IU | Работает | +| PC | Не работает | +| PY | Не работает | + +2. Попробовать с отключенным python -> Работает + +3. CLI -> Перенесено из ветки utbot-python в том же виде. В дальнейшем можно либо выделить cli/python в отдельный модуль, либо перенести в utbot-python, а внутри cli/Application.kt использовать делегатов \ No newline at end of file diff --git a/utbot-ui-commons/build.gradle.kts b/utbot-ui-commons/build.gradle.kts new file mode 100644 index 0000000000..54031e9083 --- /dev/null +++ b/utbot-ui-commons/build.gradle.kts @@ -0,0 +1,26 @@ +val kotlinLoggingVersion: String by rootProject +val ideType: String by rootProject + +plugins { + id("org.jetbrains.intellij") version "1.7.0" +} + +intellij { + version.set("212.5712.43") + type.set(ideType) + + plugins.set(listOf( + "java", + "org.jetbrains.kotlin:212-1.7.10-release-333-IJ5457.46", + "org.jetbrains.android" + )) +} + +dependencies { + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) + implementation(group = "org.jetbrains", name = "annotations", version = "16.0.2") + implementation(project(":utbot-api")) + implementation(project(":utbot-framework")) + implementation(group = "org.slf4j", name = "slf4j-api", version = "1.7.25") + api("com.jetbrains.intellij.idea:ideaIC:212.5712.43") +} diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt new file mode 100644 index 0000000000..9ad57e9a9d --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/language/agnostic/LanguageAssistant.kt @@ -0,0 +1,42 @@ +package org.utbot.intellij.plugin.language.agnostic + +import com.intellij.lang.Language +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CommonDataKeys + +abstract class LanguageAssistant { + + abstract fun update(e: AnActionEvent) + abstract fun actionPerformed(e: AnActionEvent) + + companion object { + private val languages = mutableMapOf() + + fun get(e: AnActionEvent): LanguageAssistant? { + e.getData(CommonDataKeys.PSI_FILE)?.language?.let { language -> + if (!languages.containsKey(language.id)) { + loadWithException(language)?.let { + languages.put(language.id, it.kotlin.objectInstance as LanguageAssistant) + } + } + return languages[language.id] + } + return null + } + } +} + +private fun loadWithException(language: Language): Class<*>? { + try { + return when (language.id) { + "Python" -> Class.forName("org.utbot.intellij.plugin.language.python.PythonLanguageAssistant") + "ECMAScript 6" -> Class.forName("org.utbot.intellij.plugin.language.js.JsLanguageAssistant") + "JAVA", "Kotlin" -> Class.forName("org.utbot.intellij.plugin.language.JvmLanguageAssistant") + else -> error("Unknown language id: ${language.id}") + } + } catch (e: ClassNotFoundException) { + // todo use logger + println("Language ${language.id} is disabled") + } + return null +} \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt new file mode 100644 index 0000000000..c8025a46ad --- /dev/null +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/models/BaseTestModel.kt @@ -0,0 +1,36 @@ +package org.utbot.intellij.plugin.models + +import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.psi.PsiClass +import org.jetbrains.kotlin.idea.core.getPackage +import org.utbot.framework.plugin.api.CodegenLanguage + +val PsiClass.packageName: String get() = this.containingFile.containingDirectory.getPackage()?.qualifiedName ?: "" + +open class BaseTestsModel( + val project: Project, + val srcModule: Module, + potentialTestModules: List, + var srcClasses: Set = emptySet(), +) { + // GenerateTestsModel is supposed to be created with non-empty list of potentialTestModules. + // Otherwise, the error window is supposed to be shown earlier. + var testModule: Module = potentialTestModules.firstOrNull() ?: error("Empty list of test modules in model") + + var testSourceRoot: VirtualFile? = null + var testPackageName: String? = null + open lateinit var codegenLanguage: CodegenLanguage + fun setSourceRootAndFindTestModule(newTestSourceRoot: VirtualFile?) { + requireNotNull(newTestSourceRoot) + testSourceRoot = newTestSourceRoot + testModule = ModuleUtil.findModuleForFile(newTestSourceRoot, project) + ?: error("Could not find module for $newTestSourceRoot") + } + + val isMultiPackage: Boolean by lazy { + srcClasses.map { it.packageName }.distinct().size != 1 + } +} diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt similarity index 100% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt similarity index 100% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt similarity index 88% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index c223176153..0780460281 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -10,18 +10,18 @@ import com.intellij.openapi.ui.FixedSizeButton import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.ui.ColoredListCellRenderer +import com.intellij.util.ui.UIUtil import com.intellij.ui.SimpleTextAttributes import com.intellij.util.ArrayUtil -import com.intellij.util.ui.UIUtil import java.io.File import javax.swing.DefaultComboBoxModel import javax.swing.JList import org.jetbrains.kotlin.idea.util.projectStructure.allModules import org.utbot.common.PathUtil +import org.utbot.intellij.plugin.models.BaseTestsModel import org.utbot.intellij.plugin.models.GenerateTestsModel import org.utbot.intellij.plugin.ui.utils.TestSourceRoot import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot -import org.utbot.intellij.plugin.ui.utils.isBuildWithGradle import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : @@ -30,10 +30,8 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : private val SET_TEST_FOLDER = "set test folder" init { - if (model.project.isBuildWithGradle) { - setButtonEnabled(false) - UIUtil.findComponentOfType(this, FixedSizeButton::class.java)?.toolTipText = "Please define custom test source root via Gradle" - } + setButtonEnabled(false) + UIUtil.findComponentOfType(this, FixedSizeButton::class.java)?.toolTipText = "Please define custom test source root via Gradle" childComponent.isEditable = false childComponent.renderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer( @@ -56,8 +54,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } } - val suggestedModules = - if (model.project.isBuildWithGradle) model.project.allModules() else model.potentialTestModules + val suggestedModules = model.project.allModules() val testRoots = suggestedModules.flatMap { it.suitableTestSourceRoots() @@ -97,7 +94,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : } } - private fun createNewTestSourceRoot(model: GenerateTestsModel): VirtualFile? = + private fun createNewTestSourceRoot(model: BaseTestsModel): VirtualFile? = ReadAction.compute { val desc = FileChooserDescriptor(false, true, false, false, false, false) val initialFile = model.project.guessProjectDir() @@ -118,7 +115,7 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : childComponent.model = DefaultComboBoxModel(ArrayUtil.toObjectArray(comboItems)) } - private fun formatUrl(virtualFile: VirtualFile, model: GenerateTestsModel): String { + private fun formatUrl(virtualFile: VirtualFile, model: BaseTestsModel): String { var directoryUrl = if (virtualFile is FakeVirtualFile) { virtualFile.parent.presentableUrl + File.separatorChar + virtualFile.name } else { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt similarity index 100% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ErrorUtils.kt diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt similarity index 99% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt index eb371dc4e8..81efc6dbd2 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt @@ -4,8 +4,6 @@ import org.utbot.common.PathUtil.toPath import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.intellij.plugin.ui.CommonErrorNotifier -import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.externalSystem.model.ProjectSystemId import com.intellij.openapi.externalSystem.util.ExternalSystemApiUtil @@ -29,11 +27,12 @@ import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.util.PathUtil.getParentPath import java.nio.file.Path import mu.KotlinLogging -import org.jetbrains.android.sdk.AndroidSdkType import org.jetbrains.jps.model.module.JpsModuleSourceRootType import org.jetbrains.kotlin.config.KotlinFacetSettingsProvider import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.platform.TargetPlatformVersion +import org.utbot.intellij.plugin.ui.CommonErrorNotifier +import org.utbot.intellij.plugin.ui.UnsupportedJdkNotifier private val logger = KotlinLogging.logger {} @@ -167,7 +166,8 @@ private const val dedicatedTestSourceRootName = "utbot_tests" fun Module.addDedicatedTestRoot(testSourceRoots: MutableList, language: CodegenLanguage): VirtualFile? { // Don't suggest new test source roots for Gradle project where 'unexpected' test roots won't work - if (project.isBuildWithGradle) return null + //todo we asked Vassiliy to rewrite + //if (project.isBuildWithGradle) return null // Dedicated test root already exists if (testSourceRoots.any { root -> root.dir.name == dedicatedTestSourceRootName }) return null diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt similarity index 97% rename from utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt rename to utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt index 4d7d19c28f..e687656721 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt @@ -1,6 +1,5 @@ package org.utbot.intellij.plugin.ui.utils -import org.utbot.framework.plugin.api.CodegenLanguage import com.intellij.openapi.roots.SourceFolder import org.jetbrains.jps.model.java.JavaResourceRootProperties import org.jetbrains.jps.model.java.JavaResourceRootType @@ -11,6 +10,7 @@ import org.jetbrains.kotlin.config.ResourceKotlinRootType import org.jetbrains.kotlin.config.SourceKotlinRootType import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.config.TestSourceKotlinRootType +import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.intellij.plugin.util.IntelliJApiHelper val sourceRootTypes: Set> = setOf(JavaSourceRootType.SOURCE, SourceKotlinRootType) @@ -25,6 +25,7 @@ fun CodegenLanguage.testRootType(): JpsModuleSourceRootType JavaSourceRootType.TEST_SOURCE CodegenLanguage.KOTLIN -> TestSourceKotlinRootType + else -> TestSourceKotlinRootType } /** @@ -34,6 +35,7 @@ fun CodegenLanguage.testResourcesRootType(): JpsModuleSourceRootType JavaResourceRootType.TEST_RESOURCE CodegenLanguage.KOTLIN -> TestResourceKotlinRootType + else -> TestResourceKotlinRootType } /** From eee8f85944c22c1d5f889bf41c1997dac5e3c025 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Wed, 5 Oct 2022 12:12:11 +0300 Subject: [PATCH 02/98] Add CodeGenLanguage.id --- .../kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt | 1 + .../org/utbot/framework/plugin/api/KotlinCodeLanguage.kt | 3 ++- utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt index b9fc202cb7..ba86ece2b7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt @@ -17,6 +17,7 @@ import org.utbot.framework.plugin.api.utils.testClassNameGenerator object JavaCodeLanguage : CodeGenLanguage() { override val displayName: String = "Java" + override val id: String = "Java" override val extension: String get() = ".java" diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt index a7a2717841..3da17be6f2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt @@ -15,7 +15,8 @@ import org.utbot.framework.codegen.model.visitor.CgRendererContext import org.utbot.framework.plugin.api.utils.testClassNameGenerator object KotlinCodeLanguage : CodeGenLanguage() { - override val displayName: String = "Kotlin" + override val displayName: String = "Kotlin (experimental)" + override val id: String = "Kotlin" override val extension: String get() = ".kt" diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt index 359b66bc1a..33161ba98a 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt @@ -18,6 +18,7 @@ import org.utbot.framework.plugin.api.utils.testClassNameGenerator object JsCodeLanguage : CodeGenLanguage() { override val outerMostTestClassContent: TestClassContext = TestClassContext() override val displayName: String = "JavaScript" + override val id: String = "JavaScript" override val extension: String get() = ".js" From 08ccb654524ae07887347fddad1cac5593b3e918 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Wed, 5 Oct 2022 12:21:55 +0300 Subject: [PATCH 03/98] Update Renderers --- .../codegen/model/visitor/CgAbstractRenderer.kt | 9 +++------ .../src/main/kotlin/framework/codegen/JsCodeLanguage.kt | 2 +- .../codegen/model/constructor/visitor/CgJsRenderer.kt | 2 +- .../model/constructor/visitor/CgPythonRenderer.kt | 4 ++-- 4 files changed, 7 insertions(+), 10 deletions(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt index a72e40fb70..fceb06c87b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt @@ -938,7 +938,8 @@ abstract class CgAbstractRenderer( context: CgContext, printer: CgPrinter = CgPrinterImpl() ): CgAbstractRenderer { - return context.codeGenLanguage.cgRenderer(context, printer) + val rendererContext = CgRendererContext.fromCgContext(context) + return makeRenderer(rendererContext, printer) } fun makeRenderer( @@ -951,11 +952,7 @@ abstract class CgAbstractRenderer( } private fun makeRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer { - return when (context.codegenLanguage) { - CodegenLanguage.JAVA -> CgJavaRenderer(context, printer) - CodegenLanguage.KOTLIN -> CgKotlinRenderer(context, printer) - else -> throw UnsupportedOperationException() - } + return context.codeGenLanguage.cgRenderer(context, printer) } /** diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt index 33161ba98a..9fd1a6a6de 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt @@ -40,7 +40,7 @@ object JsCodeLanguage : CodeGenLanguage() { return testClassNameGenerator(testClassCustomName, testClassPackageName, classUnderTest) } - override fun cgRenderer(context: CgContext, printer: CgPrinter): CgAbstractRenderer = CgJsRenderer(context, printer) + override fun cgRenderer(context: CgRendererContext, printer: CgPrinter): CgAbstractRenderer = CgJsRenderer(context, printer) override fun getCallableAccessManagerBy(context: CgContext) = JsCgCallableAccessManager(context) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt index b56036556a..40500ca8ba 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -50,7 +50,7 @@ import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.TypeParameters import settings.JsTestGenerationSettings.fileUnderTestAliases -internal class CgJsRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : +internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { override val statementEnding: String = "" diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt index f8dbe9f449..eefe0c5217 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -38,7 +38,7 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = println() println() - element.testClass.accept(this) + element.declaredClass.accept(this) } override fun visit(element: AbstractCgClass<*>) { @@ -215,7 +215,7 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = TODO("Not yet implemented") } - override fun renderClassFileImports(element: CgTestClassFile) { + private fun renderClassFileImports(element: CgTestClassFile) { element.imports .toSet() .filterIsInstance() From 301f2d585857268e3a243330d953da260b6cf7ce Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Wed, 5 Oct 2022 12:32:30 +0300 Subject: [PATCH 04/98] Move codeGenLanguage to CgRenderContext --- .../utbot/framework/codegen/model/visitor/CgRendererContext.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt index 414cd81943..88a6239f22 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgRendererContext.kt @@ -22,9 +22,9 @@ class CgRendererContext( val codegenLanguage: CodegenLanguage, val mockFrameworkUsed: Boolean, val mockFramework: MockFramework, + val codeGenLanguage: CodeGenLanguage = CodeGenLanguage.defaultItem, ) { - val codeGenLanguage: CodeGenLanguage = CodeGenLanguage.defaultItem companion object { fun fromCgContext(context: CgContext): CgRendererContext { return CgRendererContext( @@ -35,6 +35,7 @@ class CgRendererContext( generatedClass = context.outerMostTestClass, utilMethodProvider = context.utilMethodProvider, codegenLanguage = context.codegenLanguage, + codeGenLanguage = context.codeGenLanguage, mockFrameworkUsed = context.mockFrameworkUsed, mockFramework = context.mockFramework ) From ddc57d90369da112e08ec88257c4b1bcba45132f Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Wed, 5 Oct 2022 12:52:22 +0300 Subject: [PATCH 05/98] Add init to CodeLanguage --- .../kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt | 3 --- .../kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt | 4 ++++ .../org/utbot/framework/plugin/api/KotlinCodeLanguage.kt | 4 ++++ .../org/utbot/python/framework/codegen/PythonCodeLanguage.kt | 4 ++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt index f148d72819..7057726495 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/CodeLanguage.kt @@ -46,10 +46,7 @@ abstract class CodeGenLanguage : CodeGenerationSettingItem { } } } - val operatingSystem: OperatingSystem = OperatingSystem.fromSystemProperties() - // Get is mandatory because of the initialization order of the inheritors. - // Otherwise, in some cases we could get an incorrect value companion object : CodeGenerationSettingBox { override val defaultItem: CodeGenLanguage get() = allItems.first() override val allItems: List = CodegenLanguageProvider.allItems.toList() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt index ba86ece2b7..1cf0d1e80d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/JavaCodeLanguage.kt @@ -49,4 +49,8 @@ object JavaCodeLanguage : CodeGenLanguage() { } override val defaultTestFramework: TestFramework = Junit5 + + init { + CodegenLanguageProvider.allItems.add(this) + } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt index 3da17be6f2..58c44672d8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/KotlinCodeLanguage.kt @@ -47,4 +47,8 @@ object KotlinCodeLanguage : CodeGenLanguage() { } override val defaultTestFramework = Junit5 + + init { + CodegenLanguageProvider.allItems.add(this) + } } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt index fc29f4bb0b..033b0e82f8 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/PythonCodeLanguage.kt @@ -7,6 +7,7 @@ import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer import org.utbot.framework.codegen.model.visitor.CgRendererContext import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodeGenLanguage +import org.utbot.framework.plugin.api.CodegenLanguageProvider import org.utbot.python.framework.codegen.model.Pytest import org.utbot.python.framework.codegen.model.Unittest import org.utbot.python.framework.codegen.model.constructor.name.PythonCgNameGenerator @@ -58,4 +59,7 @@ object PythonCodeLanguage : CodeGenLanguage() { override val defaultTestFramework = Unittest + init { + CodegenLanguageProvider.allItems.add(this) + } } \ No newline at end of file From 8086a6317886115051ce4f1a939b54ae6e2b56fe Mon Sep 17 00:00:00 2001 From: Denis Fokin Date: Wed, 5 Oct 2022 14:20:31 +0300 Subject: [PATCH 06/98] UTBotFamily --- .gitignore | 2 +- .../utbot/framework/plugin/api/js/JsApi.kt | 27 +-------- .../org/utbot/framework/codegen/Keywords.kt | 2 +- .../framework/codegen/model/CodeGenerator.kt | 4 +- .../model/constructor/context/CgContext.kt | 2 +- .../constructor/tree/CgMethodConstructor.kt | 1 + .../tree/CgTestClassConstructor.kt | 1 + .../constructor/tree/CgVariableConstructor.kt | 1 + .../codegen/model/visitor/UtilMethods.kt | 56 ++++++++++++------- .../concrete/MockValueConstructor.kt | 4 +- .../fields/ExecutionStateAnalyzer.kt | 8 +-- .../framework/minimization/Minimization.kt | 2 + .../org/utbot/framework/util/TestUtils.kt | 4 +- .../language/python/PythonDialogWindow.kt | 1 - .../language/python/PythonTestsModel.kt | 1 - utbot-intellij/build.gradle.kts | 26 +++++---- .../plugin/models/GenerateTestsModel.kt | 3 - .../plugin/ui/GenerateTestsDialogWindow.kt | 4 +- .../codegen/model/PythonCodeGenerator.kt | 11 ++-- .../CodeGenerationSettingItemRenderer.kt | 2 +- .../TestFolderComboWithBrowseButton.kt | 2 +- 21 files changed, 79 insertions(+), 85 deletions(-) diff --git a/.gitignore b/.gitignore index 2070e7fe6a..7092413a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,5 @@ target/ .idea/ .gradle/ *.log -*.rdgen __pycache__/ +*.rdgen diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt index 7e7b944c90..30e5f6d902 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/js/JsApi.kt @@ -96,37 +96,12 @@ class JsMethodId( override val returnType: JsClassId get() = lazyReturnType?.value ?: returnTypeNotLazy - override val isPrivate: Boolean - get() = throw UnsupportedOperationException("JavaScript does not support private methods.") - - override val isProtected: Boolean - get() = throw UnsupportedOperationException("JavaScript does not support protected methods.") - - override val isPublic: Boolean - get() = true - - override val isStatic: Boolean - get() = staticModifier - } class JsConstructorId( override var classId: JsClassId, override val parameters: List, -) : ConstructorId(classId, parameters) { - - override val returnType: JsClassId - get() = classId - - override val isPrivate: Boolean - get() = throw UnsupportedOperationException("JavaScript does not support private constructors.") - - override val isProtected: Boolean - get() = throw UnsupportedOperationException("JavaScript does not support protected constructors.") - - override val isPublic: Boolean - get() = true -} +) : ConstructorId(classId, parameters) class JsMultipleClassId(private val jsJoinedName: String) : JsClassId(jsJoinedName) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt index c85c61ab96..abcceca14e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/Keywords.kt @@ -30,7 +30,7 @@ private val kotlinModifierKeywords = setOf( "private", "protected", "public", "reified", "sealed", "suspend", "tailrec", "vararg" ) -// For now we check only hard keywords because others can be used as methods and variables identifiers +// For now, we check only hard keywords because others can be used as methods and variables identifiers private val kotlinKeywords = kotlinHardKeywords fun isLanguageKeyword(word: String, codegenLanguage: CodeGenLanguage): Boolean = diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt index 851da3e686..eccd78e479 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/CodeGenerator.kt @@ -79,8 +79,8 @@ open class CodeGenerator( val testClassFile = CgTestClassConstructor(context).construct(testClassModel) CodeGeneratorResult( generatedCode = renderClassFile(testClassFile), - utilClassKind = UtilClassKind.fromCgContextOrNull(context), - testsGenerationReport = testClassFile.testsGenerationReport + testsGenerationReport = testClassFile.testsGenerationReport, + utilClassKind = UtilClassKind.fromCgContextOrNull(context) ) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt index 1f2b7d1870..817fe3ffac 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/context/CgContext.kt @@ -417,7 +417,7 @@ interface CgContextOwner { */ data class CgContext( override val classUnderTest: ClassId, - val generateUtilClassFile: Boolean, + val generateUtilClassFile: Boolean = false, override var currentExecutable: ExecutableId? = null, override val collectedExceptions: MutableSet = mutableSetOf(), override val collectedMethodAnnotations: MutableSet = mutableSetOf(), diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt index b5a8633569..3d60f25bc9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgMethodConstructor.kt @@ -1655,6 +1655,7 @@ open class CgMethodConstructor(val context: CgContext) : CgContextOwner by conte testSet.executions.any { it.result is UtExecutionFailure } + protected final fun testMethod( /** * Determines [CgTestMethodType] for current execution according to its success or failure. */ diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index fad443f048..c8bd25b1d7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -326,6 +326,7 @@ open class CgTestClassConstructor(val context: CgContext) : is Junit4 -> testFrameworkManagers.getOrPut(context) { Junit4Manager(context) } is Junit5 -> testFrameworkManagers.getOrPut(context) { Junit5Manager(context) } is TestNg -> testFrameworkManagers.getOrPut(context) { TestNgManager(context) } + else -> throw UnsupportedOperationException() } fun getMockFrameworkManagerBy(context: CgContext) = mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt index 56cd08a28b..9b523192dc 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgVariableConstructor.kt @@ -303,6 +303,7 @@ open class CgVariableConstructor(val context: CgContext) : return null } + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt index 57724f0872..ef497a19e7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/UtilMethods.kt @@ -125,7 +125,7 @@ private fun getEnumConstantByName(visibility: Visibility, language: CodegenLangu } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguage): String = @@ -176,7 +176,7 @@ private fun getStaticFieldValue(visibility: Visibility, language: CodegenLanguag } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): String = @@ -211,7 +211,7 @@ private fun getFieldValue(visibility: Visibility, language: CodegenLanguage): St } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun setStaticField(visibility: Visibility, language: CodegenLanguage): String = @@ -263,7 +263,7 @@ private fun setStaticField(visibility: Visibility, language: CodegenLanguage): S } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun setField(visibility: Visibility, language: CodegenLanguage): String = @@ -298,7 +298,7 @@ private fun setField(visibility: Visibility, language: CodegenLanguage): String } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun createArray(visibility: Visibility, language: CodegenLanguage): String = @@ -333,7 +333,7 @@ private fun createArray(visibility: Visibility, language: CodegenLanguage): Stri } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun createInstance(visibility: Visibility, language: CodegenLanguage): String = @@ -356,7 +356,7 @@ private fun createInstance(visibility: Visibility, language: CodegenLanguage): S } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage): String = @@ -379,7 +379,7 @@ private fun getUnsafeInstance(visibility: Visibility, language: CodegenLanguage) } """ } - else -> "" + else -> throw UnsupportedOperationException() }.trimIndent() /** @@ -600,7 +600,7 @@ private fun deepEquals( } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -643,7 +643,7 @@ private fun arraysDeepEquals(visibility: Visibility, language: CodegenLanguage): } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -684,7 +684,7 @@ private fun iterablesDeepEquals(visibility: Visibility, language: CodegenLanguag } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -729,7 +729,7 @@ private fun streamsDeepEquals(visibility: Visibility, language: CodegenLanguage) } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -786,7 +786,7 @@ private fun mapsDeepEquals(visibility: Visibility, language: CodegenLanguage): S } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): String = @@ -825,7 +825,7 @@ private fun hasCustomEquals(visibility: Visibility, language: CodegenLanguage): } """.trimIndent() } - else -> "" + else -> throw UnsupportedOperationException() } private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = @@ -840,7 +840,7 @@ private fun getArrayLength(visibility: Visibility, language: CodegenLanguage) = """ ${visibility by language}fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) """.trimIndent() - else -> "" + else -> throw UnsupportedOperationException() } private fun buildStaticLambda(visibility: Visibility, language: CodegenLanguage) = @@ -951,6 +951,7 @@ private fun buildStaticLambda(visibility: Visibility, language: CodegenLanguage) return handle.invokeWithArguments(*capturedValues) } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun buildLambda(visibility: Visibility, language: CodegenLanguage) = @@ -1085,6 +1086,7 @@ private fun buildLambda(visibility: Visibility, language: CodegenLanguage) = return handle.invokeWithArguments(*capturedValues) } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getLookupIn(language: CodegenLanguage) = @@ -1127,6 +1129,7 @@ private fun getLookupIn(language: CodegenLanguage) = return lookup } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getLambdaCapturedArgumentTypes(language: CodegenLanguage) = @@ -1169,6 +1172,7 @@ private fun getLambdaCapturedArgumentTypes(language: CodegenLanguage) = .toTypedArray() } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getLambdaCapturedArgumentValues(language: CodegenLanguage) = @@ -1195,6 +1199,7 @@ private fun getLambdaCapturedArgumentValues(language: CodegenLanguage) = .toTypedArray() } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getInstantiatedMethodType(language: CodegenLanguage) = @@ -1242,6 +1247,7 @@ private fun getInstantiatedMethodType(language: CodegenLanguage) = return java.lang.invoke.MethodType.methodType(lambdaMethod.returnType, instantiatedMethodParamTypes) } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getLambdaMethod(language: CodegenLanguage) = @@ -1272,6 +1278,7 @@ private fun getLambdaMethod(language: CodegenLanguage) = ?: throw IllegalArgumentException("No lambda method named ${'$'}lambdaName was found in class: ${'$'}{declaringClass.canonicalName}") } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun getSingleAbstractMethod(language: CodegenLanguage) = @@ -1306,6 +1313,7 @@ private fun getSingleAbstractMethod(language: CodegenLanguage) = return abstractMethods[0] } """.trimIndent() + else -> throw UnsupportedOperationException() } private fun capturedArgumentClass(language: CodegenLanguage) = @@ -1337,6 +1345,7 @@ private fun capturedArgumentClass(language: CodegenLanguage) = data class CapturedArgument(val type: Class<*>, val value: Any?) """.trimIndent() } + else -> throw UnsupportedOperationException() } internal fun CgContextOwner.importUtilMethodDependencies(id: MethodId) { @@ -1377,27 +1386,27 @@ private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( Arrays::class.id ) CodegenLanguage.KOTLIN -> listOf(fieldClassId, Arrays::class.id) - else -> emptyList() + else -> throw UnsupportedOperationException() } arraysDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(java.lang.reflect.Array::class.id, Set::class.id) CodegenLanguage.KOTLIN -> listOf(java.lang.reflect.Array::class.id) - else -> emptyList() + else -> throw UnsupportedOperationException() } iterablesDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Iterable::class.id, Iterator::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() - else -> emptyList() + else -> throw UnsupportedOperationException() } streamsDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(java.util.stream.BaseStream::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() - else -> emptyList() + else -> throw UnsupportedOperationException() } mapsDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Map::class.id, Iterator::class.id, Set::class.id) CodegenLanguage.KOTLIN -> emptyList() - else -> emptyList() + else -> throw UnsupportedOperationException() } hasCustomEqualsMethodId -> emptyList() getArrayLengthMethodId -> listOf(java.lang.reflect.Array::class.id) @@ -1407,6 +1416,7 @@ private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id ) CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) + else -> throw UnsupportedOperationException() } buildLambdaMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf( @@ -1414,18 +1424,22 @@ private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( MethodHandle::class.id, CallSite::class.id, LambdaMetafactory::class.id ) CodegenLanguage.KOTLIN -> listOf(MethodType::class.id, LambdaMetafactory::class.id) + else -> throw UnsupportedOperationException() } getLookupInMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(MethodHandles::class.id, Field::class.id, Modifier::class.id) CodegenLanguage.KOTLIN -> listOf(MethodHandles::class.id, Modifier::class.id) + else -> throw UnsupportedOperationException() } getLambdaCapturedArgumentTypesMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(LambdaMetafactory::class.id) CodegenLanguage.KOTLIN -> listOf(LambdaMetafactory::class.id) + else -> throw UnsupportedOperationException() } getLambdaCapturedArgumentValuesMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Arrays::class.id) CodegenLanguage.KOTLIN -> emptyList() + else -> throw UnsupportedOperationException() } getInstantiatedMethodTypeMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf( @@ -1433,10 +1447,12 @@ private fun TestClassUtilMethodProvider.regularImportsByUtilMethod( java.util.List::class.id, Arrays::class.id, Collectors::class.id ) CodegenLanguage.KOTLIN -> listOf(Method::class.id, MethodType::class.id, LambdaMetafactory::class.id) + else -> throw UnsupportedOperationException() } getLambdaMethodMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Method::class.id, Arrays::class.id) CodegenLanguage.KOTLIN -> listOf(Method::class.id) + else -> throw UnsupportedOperationException() } getSingleAbstractMethodMethodId -> listOf( Method::class.id, java.util.List::class.id, Arrays::class.id, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt index 595dd883fa..84bbc7ad7b 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/MockValueConstructor.kt @@ -136,9 +136,7 @@ class MockValueConstructor( is UtAssembleModel -> UtConcreteValue(constructFromAssembleModel(model), model.classId.jClass) is UtLambdaModel -> UtConcreteValue(constructFromLambdaModel(model)) is UtVoidModel -> UtConcreteValue(Unit) - is PythonModel -> TODO() - is GoUtModel -> TODO() - is JsUtModel -> TODO() + // PythonModel, GoUtModel, JsUtModel may be here else -> UtConcreteValue(null, model.classId.jClass) } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt index b816baf344..10f3cbc07f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/fields/ExecutionStateAnalyzer.kt @@ -5,10 +5,7 @@ import org.utbot.common.doNotRun import org.utbot.common.unreachableBranch import org.utbot.common.workaround import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.go.GoUtModel -import org.utbot.framework.plugin.api.js.JsUtModel import org.utbot.framework.plugin.api.MissingState -import org.utbot.framework.plugin.api.python.PythonModel import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtClassRefModel @@ -269,8 +266,7 @@ fun UtModel.accept(visitor: UtModelVisitor, data: D) = visitor.run { is UtPrimitiveModel -> visit(element, data) is UtReferenceModel -> visit(element, data) is UtVoidModel -> visit(element, data) - is PythonModel -> visit(element, data) - is GoUtModel -> error("Unexpected GoUtModel: unsupported") - is JsUtModel -> error("Unexpected JsUtModel: unsupported") + // PythonModel, GoUtModel, JsUtModel may go here + else -> throw UnsupportedOperationException() } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt index 73307c6a36..8c0ea4dfca 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt @@ -234,6 +234,8 @@ private fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): I is JsUtModel -> TODO() else -> 0 is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } + // PythonModel, GoUtModel, JsUtModel may go here + else -> 0 } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt index 3c8ec55ff2..f6b6ae4740 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/util/TestUtils.kt @@ -37,7 +37,7 @@ data class Snippet(val codegenLanguage: CodegenLanguage, var text: String) { when (codegenLanguage) { CodegenLanguage.JAVA -> text.contains("import $fullyQualifiedName;") CodegenLanguage.KOTLIN -> text.contains("import $fullyQualifiedName") - else -> TODO() + else -> throw UnsupportedOperationException() } fun doesntHaveImport(fullyQualifiedName: String) = !hasImport(fullyQualifiedName) @@ -46,7 +46,7 @@ data class Snippet(val codegenLanguage: CodegenLanguage, var text: String) { when (codegenLanguage) { CodegenLanguage.JAVA -> text.contains("import static $member;") CodegenLanguage.KOTLIN -> text.contains("import $member") - else -> TODO() + else -> throw UnsupportedOperationException() } fun doesntHaveStaticImport(member: String) = !hasStaticImport(member) diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt index 0d6bfd336b..5887baa219 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogWindow.kt @@ -17,7 +17,6 @@ import com.jetbrains.python.refactoring.classes.membersManager.PyMemberInfo import com.jetbrains.python.refactoring.classes.ui.PyMemberSelectionTable import org.utbot.framework.UtSettings import org.utbot.framework.plugin.api.CodeGenerationSettingItem -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton import java.awt.BorderLayout import java.util.concurrent.TimeUnit diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt index f2c8ed10b8..c8939d0ebe 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonTestsModel.kt @@ -7,7 +7,6 @@ import com.jetbrains.python.psi.PyFile import com.jetbrains.python.psi.PyFunction import org.utbot.framework.codegen.TestFramework import org.utbot.framework.plugin.api.CodeGenLanguage -import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.intellij.plugin.models.BaseTestsModel class PythonTestsModel( diff --git a/utbot-intellij/build.gradle.kts b/utbot-intellij/build.gradle.kts index 16dd9156ef..5af6a5c48a 100644 --- a/utbot-intellij/build.gradle.kts +++ b/utbot-intellij/build.gradle.kts @@ -37,18 +37,16 @@ intellij { ) val jsPlugins = listOf( - "JavaScriptLanguage" + "JavaScript" ) - plugins.set( - when (ideType) { - "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins - "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins - "PC" -> pythonCommunityPlugins - "PU" -> pythonUltimatePlugins // something else, JS? - else -> jvmPlugins - } - ) + plugins.set(when (ideType) { + "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins + "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins + "PC" -> pythonCommunityPlugins + "PY" -> pythonUltimatePlugins // something else, JS? + else -> jvmPlugins + }) version.set("222.4167.29") type.set(ideTypeOrAndroidStudio) @@ -96,4 +94,12 @@ dependencies { //api(project(":utbot-analytics")) testImplementation("org.mock-server:mockserver-netty:5.4.1") testApi(project(":utbot-framework")) + implementation(project(":utbot-ui-commons")) + + //Family + implementation(project(":utbot-python")) + implementation(project(":utbot-intellij-python")) + +// implementation(project(":utbot-js")) +// implementation(project(":utbot-intellij-js")) } \ No newline at end of file diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt index 5999f40154..5bc3bace7d 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/models/GenerateTestsModel.kt @@ -12,9 +12,6 @@ import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project -import com.intellij.openapi.projectRoots.JavaSdkVersion -import com.intellij.openapi.vfs.VirtualFile -import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.psi.PsiClass import com.intellij.psi.PsiJavaFile import com.intellij.refactoring.util.classMembers.MemberInfo diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt index 0587b5076d..eca68d2e3d 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsDialogWindow.kt @@ -725,7 +725,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> jUnit4LibraryDescriptor(versionInProject) Junit5 -> jUnit5LibraryDescriptor(versionInProject) TestNg -> testNgLibraryDescriptor(versionInProject) - else -> throw IllegalStateException() + else -> throw UnsupportedOperationException() } selectedTestFramework.isInstalled = true @@ -784,6 +784,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> error("Parametrized tests are not supported for JUnit 4") Junit5 -> jUnit5ParametrizedTestsLibraryDescriptor(versionInProject) TestNg -> null // Parametrized tests come with TestNG by default + else -> throw UnsupportedOperationException() } selectedTestFramework.isParametrizedTestsConfigured = true @@ -991,7 +992,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Junit4 -> parametrizedTestSources.isEnabled = false Junit5, TestNg -> parametrizedTestSources.isEnabled = true - TestNg -> parametrizedTestSources.isEnabled = true else -> parametrizedTestSources.isEnabled = false } } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt index 8ee097328a..de7b3ba562 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonCodeGenerator.kt @@ -3,6 +3,8 @@ package org.utbot.python.framework.codegen.model import org.utbot.framework.codegen.* import org.utbot.framework.codegen.model.CodeGenerator import org.utbot.framework.codegen.model.TestsCodeWithTestReport +import org.utbot.framework.codegen.model.CodeGeneratorResult + import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.constructor.context.CgContext @@ -17,7 +19,7 @@ class PythonCodeGenerator( classUnderTest: ClassId, paramNames: MutableMap> = mutableMapOf(), testFramework: TestFramework = TestFramework.defaultItem, - mockFramework: MockFramework? = MockFramework.defaultItem, + mockFramework: MockFramework = MockFramework.defaultItem, staticsMocking: StaticsMocking = StaticsMocking.defaultItem, forceStaticMocking: ForceStaticMocking = ForceStaticMocking.defaultItem, generateWarningsForStaticMocking: Boolean = true, @@ -30,6 +32,7 @@ class PythonCodeGenerator( ) : CodeGenerator( classUnderTest, paramNames, + false, testFramework, mockFramework, staticsMocking, @@ -46,7 +49,7 @@ class PythonCodeGenerator( classUnderTest = classUnderTest, paramNames = paramNames, testFramework = testFramework, - mockFramework = mockFramework ?: MockFramework.MOCKITO, + mockFramework = mockFramework, codegenLanguage = codegenLanguage, codeGenLanguage = PythonCodeLanguage, parametrizedTestSource = parameterizedTestSource, @@ -63,12 +66,12 @@ class PythonCodeGenerator( cgTestSets: List, importModules: Set, testClassCustomName: String? = null, - ): TestsCodeWithTestReport = withCustomContext(testClassCustomName) { + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { context.withTestClassFileScope { val testClassModel = TestClassModel(classUnderTest, cgTestSets) context.collectedImports.addAll(importModules) val testClassFile = PythonCgTestClassConstructor(context).construct(testClassModel) - TestsCodeWithTestReport(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + CodeGeneratorResult(renderClassFile(testClassFile), testClassFile.testsGenerationReport) } } } \ No newline at end of file diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt index 24b6b0a195..48e46a1e3d 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/CodeGenerationSettingItemRenderer.kt @@ -5,7 +5,7 @@ import javax.swing.DefaultListCellRenderer import javax.swing.JList import org.utbot.framework.plugin.api.CodeGenerationSettingItem -internal class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { +class CodeGenerationSettingItemRenderer : DefaultListCellRenderer() { override fun getListCellRendererComponent( list: JList<*>?, value: Any?, diff --git a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index 0780460281..ab8c77d1c3 100644 --- a/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-ui-commons/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -24,7 +24,7 @@ import org.utbot.intellij.plugin.ui.utils.TestSourceRoot import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots -class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : +class TestFolderComboWithBrowseButton(private val model: BaseTestsModel) : ComponentWithBrowseButton>(ComboBox(), null) { private val SET_TEST_FOLDER = "set test folder" From 07081554d3d9995c673a12e64873b4c11fae5e4a Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Wed, 5 Oct 2022 16:09:24 +0300 Subject: [PATCH 07/98] Fix after merge bugs --- .../model/constructor/CgMethodTestSet.kt | 16 --- .../constructor/tree/CgFieldStateManager.kt | 2 +- .../tree/CgTestClassConstructor.kt | 31 +++--- .../framework/minimization/Minimization.kt | 1 - .../generator/CodeGenerationController.kt | 1 + .../kotlin/org/utbot/python/PythonEngine.kt | 7 +- .../framework/codegen/model/PythonDomain.kt | 4 +- .../tree/PythonCgCallableAccessManager.kt | 8 ++ .../tree/PythonCgMethodConstructor.kt | 17 ++-- .../tree/PythonCgStatementConstructor.kt | 21 +++- .../tree/PythonCgTestClassConstructor.kt | 99 +------------------ .../tree/PythonCgVariableConstructor.kt | 4 +- .../constructor/visitor/CgPythonRenderer.kt | 29 +++--- 13 files changed, 74 insertions(+), 166 deletions(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt index 989201c040..2129ae15f7 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt @@ -31,22 +31,6 @@ data class CgMethodTestSet constructor( ) { executions = from.executions } - /** - * For JavaScript purposes. - * todo: consider to remove - */ - constructor( - executableId: ExecutableId, - execs: List = emptyList(), - errors: Map = emptyMap() - ) : this( - executableId, - null, - errors, - listOf(null to execs.indices) - ) { - executions = execs - } constructor( executableId: ExecutableId, diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt index 447fcd7356..8b8d5f3afd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgFieldStateManager.kt @@ -38,7 +38,7 @@ interface CgFieldStateManager { fun rememberFinalEnvironmentState(info: StateModificationInfo) } -internal class CgFieldStateManagerImpl(val context: CgContext) +class CgFieldStateManagerImpl(val context: CgContext) : CgContextOwner by context, CgFieldStateManager, CgCallableAccessManager by getCallableAccessManagerBy(context), diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index c8bd25b1d7..3d5d54901f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -1,23 +1,18 @@ package org.utbot.framework.codegen.model.constructor.tree -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.Junit5 import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.constructor.builtin.TestClassUtilMethodProvider import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.name.CgNameGenerator -import org.utbot.framework.codegen.model.constructor.name.CgNameGeneratorImpl import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.clearContextRelatedStorage import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getMethodConstructorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getStatementConstructorBy import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getTestFrameworkManagerBy import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor -import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructorImpl import org.utbot.framework.codegen.model.tree.CgAuxiliaryClass import org.utbot.framework.codegen.model.tree.CgExecutableUnderTestCluster import org.utbot.framework.codegen.model.tree.CgMethod @@ -293,7 +288,7 @@ open class CgTestClassConstructor(val context: CgContext) : protected val CgMethodTestSet.allErrors: Map get() = errors + codeGenerationErrors.getOrDefault(this, mapOf()) - internal object CgComponents { + object CgComponents { /** * Clears all stored data for current [CgContext]. * As far as context is created per class under test, @@ -318,20 +313,24 @@ open class CgTestClassConstructor(val context: CgContext) : private val variableConstructors: MutableMap = mutableMapOf() private val methodConstructors: MutableMap = mutableMapOf() - fun getNameGeneratorBy(context: CgContext) = nameGenerators.getOrPut(context) { CgNameGeneratorImpl(context) } - fun getCallableAccessManagerBy(context: CgContext) = callableAccessManagers.getOrPut(context) { CgCallableAccessManagerImpl(context) } - fun getStatementConstructorBy(context: CgContext) = statementConstructors.getOrPut(context) { CgStatementConstructorImpl(context) } - - fun getTestFrameworkManagerBy(context: CgContext) = when (context.testFramework) { - is Junit4 -> testFrameworkManagers.getOrPut(context) { Junit4Manager(context) } - is Junit5 -> testFrameworkManagers.getOrPut(context) { Junit5Manager(context) } - is TestNg -> testFrameworkManagers.getOrPut(context) { TestNgManager(context) } - else -> throw UnsupportedOperationException() + fun getNameGeneratorBy(context: CgContext) = nameGenerators.getOrPut(context) { + context.codeGenLanguage.getNameGeneratorBy(context) + } + fun getCallableAccessManagerBy(context: CgContext) = callableAccessManagers.getOrPut(context) { + context.codeGenLanguage.getCallableAccessManagerBy(context) + } + fun getStatementConstructorBy(context: CgContext) = statementConstructors.getOrPut(context) { + context.codeGenLanguage.getStatementConstructorBy(context) } + fun getTestFrameworkManagerBy(context: CgContext) = + testFrameworkManagers.getOrDefault(context, context.codeGenLanguage.managerByFramework(context)) + fun getMockFrameworkManagerBy(context: CgContext) = mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) } fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { CgVariableConstructor(context) } - fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { CgMethodConstructor(context) } + fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { + context.codeGenLanguage.getMethodConstructorBy(context) + } } } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt index 8c0ea4dfca..42618ca3ba 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/minimization/Minimization.kt @@ -232,7 +232,6 @@ private fun UtModel.calculateSize(used: MutableSet = mutableSetOf()): I is PythonModel -> TODO() is GoUtModel -> TODO() is JsUtModel -> TODO() - else -> 0 is UtLambdaModel -> 1 + capturedValues.sumOf { it.calculateSize(used) } // PythonModel, GoUtModel, JsUtModel may go here else -> 0 diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt index 83f05c7221..6ecec94d64 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerationController.kt @@ -478,6 +478,7 @@ object CodeGenerationController { get() = when (this) { CodegenLanguage.JAVA -> JavaFileType.INSTANCE CodegenLanguage.KOTLIN -> KotlinFileType.INSTANCE + else -> throw UnsupportedOperationException() } private fun waitForCountDown(latch: CountDownLatch, timeout: Long = 5, timeUnit: TimeUnit = TimeUnit.SECONDS, indicator : ProgressIndicator, action: Runnable) { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt index 0a0c5538b1..b441f7bbaf 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEngine.kt @@ -5,15 +5,12 @@ import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.python.NormalizedPythonAnnotation import org.utbot.framework.plugin.api.python.PythonTreeModel import org.utbot.framework.plugin.api.python.pythonAnyClassId -import org.utbot.fuzzer.FuzzedConcreteValue -import org.utbot.fuzzer.FuzzedMethodDescription -import org.utbot.fuzzer.fuzz +import org.utbot.fuzzer.* import org.utbot.python.code.AnnotationProcessor.getModulesFromAnnotation import org.utbot.python.providers.defaultPythonModelProvider import org.utbot.python.utils.camelToSnakeCase import org.utbot.summary.fuzzer.names.MethodBasedNameSuggester import org.utbot.summary.fuzzer.names.ModelBasedNameSuggester -import org.utbot.fuzzer.FuzzedValue import java.lang.Long.max private val logger = KotlinLogging.logger {} @@ -138,7 +135,7 @@ class PythonEngine( } yield( - UtExecution( + UtFuzzedExecution( stateBefore = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), stateAfter = EnvironmentModels(jobResult.thisObject, jobResult.modelList, emptyMap()), result = result, diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt index 01360e879a..8900a4b725 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/PythonDomain.kt @@ -9,7 +9,7 @@ import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.plugin.api.util.voidClassId -object Pytest : TestFramework(displayName = "pytest") { +object Pytest : TestFramework(displayName = "pytest", id = "pytest") { override val mainPackage: String = "pytest" override val assertionsClass: ClassId = pythonNoneClassId override val arraysAssertionsClass: ClassId = assertionsClass @@ -45,7 +45,7 @@ object Pytest : TestFramework(displayName = "pytest") { } } -object Unittest : TestFramework(displayName = "Unittest") { +object Unittest : TestFramework(displayName = "Unittest", id = "Unittest") { override val testSuperClass: ClassId = PythonClassId("unittest.TestCase") override val mainPackage: String = "unittest" override val assertionsClass: ClassId = PythonClassId("self") diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt index f021f2f57c..23be64f8e9 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgCallableAccessManager.kt @@ -22,6 +22,14 @@ class PythonCgCallableAccessManagerImpl(val context: CgContext) : CgCallableAcce override fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = CgIncompleteMethodCall(staticMethodId, CgThisInstance(pythonAnyClassId)) + override fun CgExpression.get(fieldId: FieldId): CgExpression { + TODO("Not yet implemented") + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + override fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { val resolvedArgs = args.resolve() val constructorCall = CgConstructorCall(this, resolvedArgs) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt index 8f6b6ffab0..faa8be8fc5 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -1,8 +1,10 @@ package org.utbot.python.framework.codegen.model.constructor.tree import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgFieldStateManagerImpl import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor import org.utbot.framework.codegen.model.tree.* +import org.utbot.framework.fields.ExecutionStateAnalyzer import org.utbot.framework.fields.StateModificationInfo import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.python.* @@ -36,8 +38,9 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex rememberInitialStaticFields(statics) context.codeGenLanguage.memoryObjects.clear() -// val stateAnalyzer = ExecutionStateAnalyzer(execution) - val modificationInfo = StateModificationInfo() // stateAnalyzer.findModifiedFields() + val stateAnalyzer = ExecutionStateAnalyzer(execution) + val modificationInfo = stateAnalyzer.findModifiedFields() + val fieldStateManager = CgFieldStateManagerImpl(context) // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states val mainBody = { substituteStaticFields(statics) @@ -51,16 +54,10 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex val name = paramNames[executableId]?.get(index) methodArguments += variableConstructor.getOrCreateVariable(param, name) } -// if (executableId is PythonMethodId) { -// existingVariableNames += executableId.name -// executableId.moduleName.split('.').forEach { -// existingVariableNames += it -// } -// } - rememberInitialEnvironmentState(modificationInfo) + fieldStateManager.rememberInitialEnvironmentState(modificationInfo) recordActualResult() generateResultAssertions() - rememberFinalEnvironmentState(modificationInfo) + fieldStateManager.rememberFinalEnvironmentState(modificationInfo) generateFieldStateAssertions() if (executableId is PythonMethodId) generatePythonTestComments(execution) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt index ec417c5e03..b50ed8971b 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgStatementConstructor.kt @@ -4,7 +4,8 @@ import fj.data.Either import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager -import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getCallableAccessManagerBy +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor.CgComponents.getNameGeneratorBy import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType import org.utbot.framework.codegen.model.tree.* @@ -12,6 +13,8 @@ import org.utbot.framework.codegen.model.util.buildExceptionHandler import org.utbot.framework.codegen.model.util.isAccessibleFrom import org.utbot.framework.codegen.model.util.resolve import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.util.* import org.utbot.python.framework.codegen.model.constructor.util.* @@ -20,9 +23,9 @@ import java.util.* class PythonCgStatementConstructorImpl(context: CgContext) : CgStatementConstructor, CgContextOwner by context, - CgCallableAccessManager by CgComponents.getCallableAccessManagerBy(context) { + CgCallableAccessManager by getCallableAccessManagerBy(context) { - private val nameGenerator = CgComponents.getNameGeneratorBy(context) + private val nameGenerator = getNameGeneratorBy(context) override fun newVar( baseType: ClassId, @@ -121,6 +124,18 @@ class PythonCgStatementConstructorImpl(context: CgContext) : currentBlock += buildCgForEachLoop(init) } + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt index 6063fb8a88..56f69cc050 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt @@ -1,7 +1,5 @@ package org.utbot.python.framework.codegen.model.constructor.tree -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor @@ -10,104 +8,9 @@ import org.utbot.framework.codegen.model.tree.* internal class PythonCgTestClassConstructor(context: CgContext) : CgTestClassConstructor(context) { override fun construct(testClassModel: TestClassModel): CgTestClassFile { return buildTestClassFile { - this.testClass = withTestClassScope { constructTestClass(testClassModel) } + this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } imports.addAll(context.collectedImports) testsGenerationReport = this@PythonCgTestClassConstructor.testsGenerationReport } } - - override fun constructTestClass(testClassModel: TestClassModel): CgTestClass { - return buildTestClass { - id = currentTestClass - - if (currentTestClass != outerMostTestClass) { - isNested = true - isStatic = testFramework.nestedClassesShouldBeStatic - testFrameworkManager.annotationForNestedClasses?.let { - currentTestClassContext.collectedTestClassAnnotations += it - } - } - if (testClassModel.nestedClasses.isNotEmpty()) { - testFrameworkManager.annotationForOuterClasses?.let { - currentTestClassContext.collectedTestClassAnnotations += it - } - } - - body = buildTestClassBody { - for (nestedClass in testClassModel.nestedClasses) { - nestedClassRegions += CgSimpleRegion( - "Tests for ${nestedClass.classUnderTest.simpleName}", - listOf( - withNestedClassScope(nestedClass) { constructTestClass(nestedClass) } - ) - ) - } - - for (testSet in testClassModel.methodTestSets) { - updateCurrentExecutable(testSet.executableId) - val currentMethodUnderTestRegions = constructTestSet(testSet) ?: continue - val executableUnderTestCluster = CgExecutableUnderTestCluster( - "Test suites for executable $currentExecutable", - currentMethodUnderTestRegions - ) - testMethodRegions += executableUnderTestCluster - } - - val additionalMethods = currentTestClassContext.cgDataProviderMethods - - dataProvidersAndUtilMethodsRegion += CgStaticsRegion( - "Data providers and utils methods", - additionalMethods - ) - } - // It is important that annotations, superclass and interfaces assignment is run after - // all methods are generated so that all necessary info is already present in the context - with (currentTestClassContext) { - annotations += collectedTestClassAnnotations - superclass = testFramework.testSuperClass - interfaces += collectedTestClassInterfaces - } - } - } - - override fun constructTestSet(testSet: CgMethodTestSet): List>? { - if (testSet.executions.isEmpty()) { - return null - } - - allExecutions = testSet.executions - - val (methodUnderTest, _, _, clustersInfo) = testSet - val regions = mutableListOf>() - - when (context.parametrizedTestSource) { - ParametrizedTestSource.DO_NOT_PARAMETRIZE -> { - for ((clusterSummary, executionIndices) in clustersInfo) { - val currentTestCaseTestMethods = mutableListOf() - emptyLineIfNeeded() - for (i in executionIndices) { - runCatching { - currentTestCaseTestMethods += methodConstructor.createTestMethod(methodUnderTest, testSet.executions[i]) - }.onFailure { e -> processFailure(testSet, e) } - } - val clusterHeader = clusterSummary?.header - val clusterContent = clusterSummary?.content - ?.split('\n') - ?.let { CgTripleSlashMultilineComment(it) } - regions += CgTestMethodCluster(clusterHeader, clusterContent, currentTestCaseTestMethods) - - testsGenerationReport.addTestsByType(testSet, currentTestCaseTestMethods) - } - } - ParametrizedTestSource.PARAMETRIZE -> {} - } - - val errors = testSet.allErrors - if (errors.isNotEmpty()) { - regions += methodConstructor.errorMethod(testSet.executableId, errors) - testsGenerationReport.addMethodErrors(testSet, errors) - } - - return regions - } } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt index d2f3f875a9..fe42874b78 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgVariableConstructor.kt @@ -1,15 +1,15 @@ package org.utbot.python.framework.codegen.model.constructor.tree import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor -import org.utbot.framework.codegen.model.constructor.util.CgComponents import org.utbot.framework.codegen.model.tree.* import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.python.* import org.utbot.python.framework.codegen.model.tree.* class PythonCgVariableConstructor(context_: CgContext) : CgVariableConstructor(context_) { - private val nameGenerator = CgComponents.getNameGeneratorBy(context) + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(context) override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { val baseName = name ?: nameGenerator.nameFrom(model.classId) diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt index eefe0c5217..7f18feeee1 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -42,7 +42,14 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = } override fun visit(element: AbstractCgClass<*>) { - TODO("Not yet implemented") + print("class ") + print(element.simpleName) + if (element.superclass != null) { + print("(${element.superclass!!.asString()})") + } + println(":") + withIndent { element.body.accept(this) } + println("") } override fun visit(element: CgCommentedAnnotation) { @@ -101,19 +108,17 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = element.expression.accept(this) } - override fun visit(element: CgTestClass) { - print("class ") - print(element.simpleName) - if (element.superclass != null) { - print("(${element.superclass!!.asString()})") + override fun visit(element: CgTestClassBody) { + // render regions for test methods + for ((i, region) in (element.testMethodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) } - println(":") - withIndent { element.body.accept(this) } - println("") - } - override fun visit(element: CgTestClassBody) { - TODO("Not yet implemented") + if (element.staticDeclarationRegions.isEmpty()) { + return + } } override fun visit(element: CgTryCatch) { From 64ade72c95cb36b65e878fd2c217ff3a5cd785a4 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 10:21:16 +0300 Subject: [PATCH 08/98] Fix bug with variableConstructors and remove ExecutionStateAnalyzer from python --- .../tree/CgTestClassConstructor.kt | 10 +++-- .../model/visitor/CgAbstractRenderer.kt | 2 +- .../tree/PythonCgMethodConstructor.kt | 4 +- .../tree/PythonCgTestClassConstructor.kt | 5 ++- .../constructor/visitor/CgPythonRenderer.kt | 41 +++++++++++++------ 5 files changed, 40 insertions(+), 22 deletions(-) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt index 3d5d54901f..568f28072f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/tree/CgTestClassConstructor.kt @@ -47,9 +47,9 @@ open class CgTestClassConstructor(val context: CgContext) : clearContextRelatedStorage() } - protected val methodConstructor = getMethodConstructorBy(context) + private val methodConstructor = getMethodConstructorBy(context) private val nameGenerator = getNameGeneratorBy(context) - protected val testFrameworkManager = getTestFrameworkManagerBy(context) + private val testFrameworkManager = getTestFrameworkManagerBy(context) protected val testsGenerationReport: TestsGenerationReport = TestsGenerationReport() @@ -184,7 +184,7 @@ open class CgTestClassConstructor(val context: CgContext) : return regions } - protected fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { + private fun processFailure(testSet: CgMethodTestSet, failure: Throwable) { codeGenerationErrors .getOrPut(testSet) { mutableMapOf() } .merge(failure.description, 1, Int::plus) @@ -327,7 +327,9 @@ open class CgTestClassConstructor(val context: CgContext) : testFrameworkManagers.getOrDefault(context, context.codeGenLanguage.managerByFramework(context)) fun getMockFrameworkManagerBy(context: CgContext) = mockFrameworkManagers.getOrPut(context) { MockFrameworkManager(context) } - fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { CgVariableConstructor(context) } + fun getVariableConstructorBy(context: CgContext) = variableConstructors.getOrPut(context) { + context.codeGenLanguage.getVariableConstructorBy(context) + } fun getMethodConstructorBy(context: CgContext) = methodConstructors.getOrPut(context) { context.codeGenLanguage.getMethodConstructorBy(context) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt index fceb06c87b..f30a23f387 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgAbstractRenderer.kt @@ -881,7 +881,7 @@ abstract class CgAbstractRenderer( protected abstract fun renderClassModality(aClass: AbstractCgClass<*>) - private fun renderMethodDocumentation(element: CgMethod) { + protected fun renderMethodDocumentation(element: CgMethod) { element.documentation.accept(this) } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt index faa8be8fc5..a60798b493 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgMethodConstructor.kt @@ -4,7 +4,6 @@ import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.tree.CgFieldStateManagerImpl import org.utbot.framework.codegen.model.constructor.tree.CgMethodConstructor import org.utbot.framework.codegen.model.tree.* -import org.utbot.framework.fields.ExecutionStateAnalyzer import org.utbot.framework.fields.StateModificationInfo import org.utbot.framework.plugin.api.* import org.utbot.framework.plugin.api.python.* @@ -38,8 +37,7 @@ class PythonCgMethodConstructor(context: CgContext) : CgMethodConstructor(contex rememberInitialStaticFields(statics) context.codeGenLanguage.memoryObjects.clear() - val stateAnalyzer = ExecutionStateAnalyzer(execution) - val modificationInfo = stateAnalyzer.findModifiedFields() + val modificationInfo = StateModificationInfo() val fieldStateManager = CgFieldStateManagerImpl(context) // TODO: move such methods to another class and leave only 2 public methods: remember initial and final states val mainBody = { diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt index 56f69cc050..080514b14f 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/tree/PythonCgTestClassConstructor.kt @@ -8,7 +8,10 @@ import org.utbot.framework.codegen.model.tree.* internal class PythonCgTestClassConstructor(context: CgContext) : CgTestClassConstructor(context) { override fun construct(testClassModel: TestClassModel): CgTestClassFile { return buildTestClassFile { - this.declaredClass = withTestClassScope { constructTestClass(testClassModel) } + this.declaredClass = withTestClassScope { + with (currentTestClassContext) { testClassSuperclass = testFramework.testSuperClass } + constructTestClass(testClassModel) + } imports.addAll(context.collectedImports) testsGenerationReport = this@PythonCgTestClassConstructor.testsGenerationReport } diff --git a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt index 7f18feeee1..e87d071520 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/framework/codegen/model/constructor/visitor/CgPythonRenderer.kt @@ -140,7 +140,7 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = } override fun visit(element: CgArrayAnnotationArgument) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(element: CgAnonymousFunction) { @@ -162,27 +162,34 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = } override fun visit(element: CgNotNullAssertion) { - TODO("Not yet implemented") + element.expression.accept(this) } override fun visit(element: CgAllocateArray) { - TODO("Not yet implemented") + print("[None] * ${element.size}") } override fun visit(element: CgAllocateInitializedArray) { - TODO("Not yet implemented") + print(" [") + element.initializer.accept(this) + print(" ]") } override fun visit(element: CgArrayInitializer) { - TODO("Not yet implemented") + val elementType = element.elementType + val elementsInLine = arrayElementsInLine(elementType) + + print("[") + element.values.renderElements(elementsInLine) + print("]") } override fun visit(element: CgSwitchCaseLabel) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(element: CgSwitchCase) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(element: CgParameterDeclaration) { @@ -199,11 +206,11 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = } override fun visit(element: CgGetJavaClass) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(element: CgGetKotlinClass) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(element: CgConstructorCall) { @@ -217,7 +224,7 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = } override fun renderStaticImport(staticImport: StaticImport) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } private fun renderClassFileImports(element: CgTestClassFile) { @@ -260,8 +267,16 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = print(")") } + override fun visit(element: CgErrorTestMethod) { + renderMethodDocumentation(element) + renderMethodSignature(element) + visit(element as CgMethod) + println("pass") + } + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { - TODO("Not yet implemented") + val returnType = element.returnType.canonicalName + println("def ${element.name}() -> $returnType: pass") } override fun visit(element: CgInnerBlock) { @@ -332,11 +347,11 @@ internal class CgPythonRenderer(context: CgRendererContext, printer: CgPrinter = override fun escapeNamePossibleKeywordImpl(s: String): String = s override fun renderClassVisibility(classId: ClassId) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun renderClassModality(aClass: AbstractCgClass<*>) { - TODO("Not yet implemented") + throw UnsupportedOperationException() } override fun visit(block: List, printNextLine: Boolean) { From 50c9a579d296bb66b0fda72a939d9e32fc44684e Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 11:19:09 +0300 Subject: [PATCH 09/98] Update generated_tests__dicts --- .../cli_utbot_tests/generated_tests__dicts.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py index 61089ec196..aba188aa11 100644 --- a/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py +++ b/utbot-python/samples/cli_utbot_tests/generated_tests__dicts.py @@ -1,30 +1,44 @@ import sys sys.path.append('samples') import builtins -import dicts import types +import dicts import unittest -class TestDictionary(unittest.TestCase): +class TestTopLevelFunctions(unittest.TestCase): + # region Test suites for executable dicts.keys + + # region + + def test_keys(self): + word = dicts.Word({str(-123456789): str(), str(1.5 + 3.5j): str(), str(b'\x80'): str(), str(): str(1e+300 * 1e+300), str('unicode remains unicode'): str(), }) + + actual = word.keys() + + self.assertEqual(['-123456789', '(1.5+3.5j)', "b'\\x80'", '', 'unicode remains unicode'], actual) + # endregion + + # endregion + # region Test suites for executable dicts.translate + # region + def test_translate(self): - dictionary = dicts.Dictionary([str(1.5 + 3.5j), str(1.5 + 3.5j), str('unicode remains unicode'), str(b'\x80'), str(-1234567890)], [{str(-123456789): str(1e+300 * 1e+300)}, {str(1.5 + 3.5j): str(), str(-123456789): str(), str(1e+300 * 1e+300): str(), str(): str(1e+300 * 1e+300)}]) + dictionary = dicts.Dictionary([str(b'\xf0\xa3\x91\x96', 'utf-8'), str(id), str(1e+300 * 1e+300)], []) - actual = dictionary.translate(str(-1234567890), str(-123456789)) + actual = dictionary.translate(str(id), str(1.5 + 3.5j)) self.assertEqual(None, actual) def test_translate_throws_t(self): - dictionary = dicts.Dictionary([str(1e+300 * 1e+300), str(1.5 + 3.5j), str(1e+300 * 1e+300), str(b'\xf0\xa3\x91\x96', 'utf-8'), str(1.5 + 3.5j), str(-1234567890)], [{str(b'\x80'): str(1e+300 * 1e+300), str(-123456789): str(1e+300 * 1e+300)}, {str(b'\x80'): str(1e+300 * 1e+300), str(b'\xf0\xa3\x91\x96', 'utf-8'): str(), str(id): str(1e+300 * 1e+300), str(): str()}, {str(b'\x80'): str(1e+300 * 1e+300), str(-123456789): str(), str(): str()}, {str(1e+300 * 1e+300): str(), str(-123456789): str(1e+300 * 1e+300)}, {str(1.5 + 3.5j): str(), str(1e+300 * 1e+300): str()}]) + dictionary = dicts.Dictionary([], [{str(): str(), str(1e+300 * 1e+300): str(1e+300 * 1e+300), str(b'\x80'): str(), str(1.5 + 3.5j): str(), }, {str(-123456789): str(), str(id): str(), str(): str(), str(-1234567890): str(), }, {str(1.5 + 3.5j): str(), str(1e+300 * 1e+300): str(), str(-1234567890): str(), str(): str(1e+300 * 1e+300), }]) - dictionary.translate(str(), str('unicode remains unicode')) + dictionary.translate(str('unicode remains unicode'), str(1.5 + 3.5j)) # raises builtins.KeyError - # endregion # endregion - From 6a6a337d7e55436239c96c797076af791616f899 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 11:19:59 +0300 Subject: [PATCH 10/98] Change targetCompatibility verstion --- build.gradle.kts | 2 +- settings.gradle | 6 +++--- utbot-cli-python/build.gradle | 2 -- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 88722424cb..ac1316eb06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ allprojects { tasks { withType { sourceCompatibility = "1.8" - targetCompatibility = "1.8" + targetCompatibility = "11" options.encoding = "UTF-8" options.compilerArgs = options.compilerArgs + "-Xlint:all" } diff --git a/settings.gradle b/settings.gradle index 0f1ac44d76..fd1fb1acba 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,8 +19,8 @@ include 'utbot-fuzzers' //include 'utbot-junit-contest' //include 'utbot-analytics' //include 'utbot-analytics-torch' -//include 'utbot-cli' -//include 'utbot-cli-python' +include 'utbot-cli' +include 'utbot-cli-python' //include 'utbot-cli-js' include 'utbot-api' include 'utbot-instrumentation' @@ -35,7 +35,7 @@ include 'utbot-rd' include 'utbot-python' //include 'utbot-js' //include 'utbot-go' -//include 'utbot-intellij-js' include 'utbot-ui-commons' include 'utbot-intellij-python' +//include 'utbot-intellij-js' diff --git a/utbot-cli-python/build.gradle b/utbot-cli-python/build.gradle index 21fc5e274b..01bfd5f7e6 100644 --- a/utbot-cli-python/build.gradle +++ b/utbot-cli-python/build.gradle @@ -17,9 +17,7 @@ configurations { } dependencies { - implementation project(':utbot-framework-api') implementation project(':utbot-framework') - implementation project(':utbot-summary') implementation project(':utbot-cli') implementation project(':utbot-python') From 9e9725cc599c1574d831b933304d662d83846e75 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 12:43:32 +0300 Subject: [PATCH 11/98] Refactor js --- build.gradle.kts | 4 +- gradle.properties | 2 +- settings.gradle | 10 ++- utbot-cli-js/build.gradle | 8 +-- .../model/constructor/CgMethodTestSet.kt | 15 ++++ .../src/main/kotlin/api/JsTestGenerator.kt | 23 +++--- .../main/kotlin/api/JsUtModelConstructor.kt | 9 +-- .../main/kotlin/codegen/JsCodeGenerator.kt | 6 +- .../framework/codegen/JsCodeLanguage.kt | 1 + .../main/kotlin/framework/codegen/JsDomain.kt | 2 +- .../tree/JsCgCallableAccessManager.kt | 14 ++-- .../tree/JsCgStatementConstructor.kt | 20 +++++- .../tree/JsCgVariableConstructor.kt | 4 +- .../model/constructor/visitor/CgJsRenderer.kt | 71 ++++++++----------- .../fuzzer/providers/JsObjectModelProvider.kt | 10 +-- 15 files changed, 110 insertions(+), 89 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index ac1316eb06..474e9da095 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ allprojects { tasks { withType { sourceCompatibility = "1.8" - targetCompatibility = "11" + targetCompatibility = "1.8" options.encoding = "UTF-8" options.compilerArgs = options.compilerArgs + "-Xlint:all" } @@ -47,7 +47,7 @@ allprojects { } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = "11" freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") allWarningsAsErrors = false } diff --git a/gradle.properties b/gradle.properties index 8dc2f9fdd2..15f94b7c87 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ kotlin.code.style=official # IU, IC, PC, PY, WS... # IC for AndroidStudio -ideType=IC +ideType=IU # In order to run Android Studion instead of Intellij Community, # specify the path to your Android Studio installation diff --git a/settings.gradle b/settings.gradle index fd1fb1acba..d0578ffc3b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,9 +19,11 @@ include 'utbot-fuzzers' //include 'utbot-junit-contest' //include 'utbot-analytics' //include 'utbot-analytics-torch' + include 'utbot-cli' include 'utbot-cli-python' -//include 'utbot-cli-js' +include 'utbot-cli-js' + include 'utbot-api' include 'utbot-instrumentation' //include 'utbot-instrumentation-tests' @@ -32,10 +34,12 @@ include 'utbot-maven' //include 'utbot-summary-tests' //include 'utbot-framework-test' include 'utbot-rd' + include 'utbot-python' -//include 'utbot-js' +include 'utbot-js' //include 'utbot-go' include 'utbot-ui-commons' + include 'utbot-intellij-python' -//include 'utbot-intellij-js' +include 'utbot-intellij-js' diff --git a/utbot-cli-js/build.gradle b/utbot-cli-js/build.gradle index ffe61ddd1c..01c0bb1d7c 100644 --- a/utbot-cli-js/build.gradle +++ b/utbot-cli-js/build.gradle @@ -17,17 +17,15 @@ configurations { } dependencies { - implementation project(':utbot-framework-api') implementation project(':utbot-framework') - implementation project(':utbot-summary') implementation project(':utbot-cli') - api project(':utbot-js') + implementation project(':utbot-js') // Without this dependency testng tests do not run. implementation group: 'com.beust', name: 'jcommander', version: '1.48' - implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4_platform_version + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4PlatformVersion implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion - implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: clikt_version + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: cliktVersion implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt index 2129ae15f7..e60bad50a1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/CgMethodTestSet.kt @@ -31,6 +31,21 @@ data class CgMethodTestSet constructor( ) { executions = from.executions } + /** + * For JavaScript purposes. + */ + constructor( + executableId: ExecutableId, + execs: List = emptyList(), + errors: Map = emptyMap() + ) : this( + executableId, + null, + errors, + listOf(null to execs.indices) + ) { + executions = execs + } constructor( executableId: ExecutableId, diff --git a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt index 3cbf51fd66..a102ff6812 100644 --- a/utbot-js/src/main/kotlin/api/JsTestGenerator.kt +++ b/utbot-js/src/main/kotlin/api/JsTestGenerator.kt @@ -27,10 +27,12 @@ import org.utbot.framework.plugin.api.js.JsMultipleClassId import org.utbot.framework.plugin.api.js.JsPrimitiveModel import org.utbot.framework.plugin.api.js.util.isJsBasic import org.utbot.framework.plugin.api.js.util.jsErrorClassId +import org.utbot.framework.plugin.api.util.isStatic import org.utbot.framework.plugin.api.util.voidClassId import org.utbot.fuzzer.FuzzedConcreteValue import org.utbot.fuzzer.FuzzedMethodDescription import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.UtFuzzedExecution import parser.JsClassAstVisitor import parser.JsFunctionAstVisitor import parser.JsFuzzerAstVisitor @@ -153,7 +155,7 @@ class JsTestGenerator( val thisInstance = makeThisInstance(execId, classId, concreteValues) val initEnv = EnvironmentModels(thisInstance, param.map { it.model }, mapOf()) testsForGenerator.add( - UtExecution( + UtFuzzedExecution( stateBefore = initEnv, stateAfter = initEnv, result = result, @@ -189,20 +191,17 @@ class JsTestGenerator( classId.allConstructors.first().parameters.isEmpty() -> { val id = JsObjectModelProvider.idGenerator.asInt val constructor = classId.allConstructors.first() - val instantiationChain = mutableListOf() + val instantiationCall = UtExecutableCallModel( + instance = null, + executable = constructor, + params = emptyList(), + ) UtAssembleModel( id = id, classId = constructor.classId, modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), - instantiationChain = instantiationChain - ).apply { - instantiationChain += UtExecutableCallModel( - instance = null, - executable = constructor, - params = emptyList(), - returnValue = this - ) - } + instantiationCall = instantiationCall, + ) } else -> { @@ -424,7 +423,7 @@ class JsTestGenerator( private fun UtAssembleModel.toParamString(): String = with(this) { val callConstructorString = "new $fileUnderTestAliases.${classId.name}" - val paramsString = (instantiationChain.first() as UtExecutableCallModel).params.joinToString( + val paramsString = instantiationCall.params.joinToString( prefix = "(", postfix = ")", ) { diff --git a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt index 8f0100f4f7..28aad6fc21 100644 --- a/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt +++ b/utbot-js/src/main/kotlin/api/JsUtModelConstructor.kt @@ -52,15 +52,12 @@ class JsUtModelConstructor : UtModelConstructorInterface { construct(it, JsEmptyClassId()) } val id = JsObjectModelProvider.idGenerator.asInt - val instantiationChain = mutableListOf() + val instantiationCall = UtExecutableCallModel(null, constructor, values) return UtAssembleModel( id = id, classId = constructor.classId, modelName = "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), - instantiationChain = instantiationChain, - modificationsChain = mutableListOf() - ).apply { - instantiationChain += UtExecutableCallModel(null, constructor, values, this) - } + instantiationCall = instantiationCall, + ) } } \ No newline at end of file diff --git a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt index 1750f283d6..c129242ec6 100644 --- a/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt +++ b/utbot-js/src/main/kotlin/codegen/JsCodeGenerator.kt @@ -9,7 +9,7 @@ import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour import org.utbot.framework.codegen.StaticsMocking import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.model.TestsCodeWithTestReport +import org.utbot.framework.codegen.model.CodeGeneratorResult import org.utbot.framework.codegen.model.constructor.CgMethodTestSet import org.utbot.framework.codegen.model.constructor.TestClassModel import org.utbot.framework.codegen.model.constructor.context.CgContext @@ -59,10 +59,10 @@ class JsCodeGenerator( fun generateAsStringWithTestReport( cgTestSets: List, testClassCustomName: String? = null, - ): TestsCodeWithTestReport = withCustomContext(testClassCustomName) { + ): CodeGeneratorResult = withCustomContext(testClassCustomName) { val testClassModel = TestClassModel(classUnderTest, cgTestSets) val testClassFile = CgTestClassConstructor(context).construct(testClassModel) - TestsCodeWithTestReport(renderClassFile(testClassFile), testClassFile.testsGenerationReport) + CodeGeneratorResult(renderClassFile(testClassFile), testClassFile.testsGenerationReport) } private fun withCustomContext(testClassCustomName: String? = null, block: () -> R): R { diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt index 9fd1a6a6de..b079784c75 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/JsCodeLanguage.kt @@ -11,6 +11,7 @@ import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.tree.TestFrameworkManager import org.utbot.framework.codegen.model.util.CgPrinter import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodeGenLanguage import org.utbot.framework.plugin.api.utils.testClassNameGenerator diff --git a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt index 17ea549ec9..82d04a5ca7 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/JsDomain.kt @@ -9,7 +9,7 @@ import org.utbot.framework.plugin.api.js.util.jsErrorClassId import org.utbot.framework.plugin.api.js.util.jsUndefinedClassId -object Mocha : TestFramework("Mocha") { +object Mocha : TestFramework(id = "Mocha", displayName = "Mocha") { override val mainPackage = "" override val assertionsClass = jsUndefinedClassId override val arraysAssertionsClass = jsUndefinedClassId diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt index cb5a005354..1f3595ff16 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgCallableAccessManager.kt @@ -4,13 +4,11 @@ import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager import org.utbot.framework.codegen.model.constructor.tree.CgIncompleteMethodCall -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgMethodCall +import org.utbot.framework.codegen.model.tree.* import org.utbot.framework.codegen.model.util.resolve import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId +import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MethodId class JsCgCallableAccessManager(context: CgContext) : CgCallableAccessManager, @@ -22,6 +20,14 @@ class JsCgCallableAccessManager(context: CgContext) : CgCallableAccessManager, override operator fun ClassId.get(staticMethodId: MethodId): CgIncompleteMethodCall = CgIncompleteMethodCall(staticMethodId, null) + override fun CgExpression.get(fieldId: FieldId): CgExpression { + TODO("Not yet implemented") + } + + override fun ClassId.get(fieldId: FieldId): CgStaticFieldAccess { + TODO("Not yet implemented") + } + override operator fun ConstructorId.invoke(vararg args: Any?): CgExecutableCall { val resolvedArgs = args.resolve() val constructorCall = CgConstructorCall(this, resolvedArgs) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt index 0847bdef10..51c20fadea 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgStatementConstructor.kt @@ -5,7 +5,7 @@ import framework.codegen.model.constructor.util.plus import org.utbot.framework.codegen.model.constructor.context.CgContext import org.utbot.framework.codegen.model.constructor.context.CgContextOwner import org.utbot.framework.codegen.model.constructor.tree.CgCallableAccessManager -import org.utbot.framework.codegen.model.constructor.util.CgComponents +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor import org.utbot.framework.codegen.model.constructor.util.CgStatementConstructor import org.utbot.framework.codegen.model.constructor.util.ExpressionWithType import org.utbot.framework.codegen.model.tree.CgAnnotation @@ -40,14 +40,16 @@ import org.utbot.framework.codegen.model.tree.buildWhileLoop import org.utbot.framework.codegen.model.util.buildExceptionHandler import org.utbot.framework.codegen.model.util.resolve import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.ExecutableId +import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.UtModel class JsCgStatementConstructor(context: CgContext) : CgStatementConstructor, CgContextOwner by context, - CgCallableAccessManager by CgComponents.getCallableAccessManagerBy(context) { + CgCallableAccessManager by CgTestClassConstructor.CgComponents.getCallableAccessManagerBy(context) { - private val nameGenerator = CgComponents.getNameGeneratorBy(context) + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(context) override fun newVar( baseType: ClassId, @@ -156,6 +158,18 @@ class JsCgStatementConstructor(context: CgContext) : throw UnsupportedOperationException("JavaScript does not have forEach loops") } + override fun getClassOf(classId: ClassId): CgExpression { + TODO("Not yet implemented") + } + + override fun createFieldVariable(fieldId: FieldId): CgVariable { + TODO("Not yet implemented") + } + + override fun createExecutableVariable(executableId: ExecutableId, arguments: List): CgVariable { + TODO("Not yet implemented") + } + override fun tryBlock(init: () -> Unit): CgTryCatch = tryBlock(init, null) override fun tryBlock(init: () -> Unit, resources: List?): CgTryCatch = diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt index bb10697b89..f3e0152046 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/tree/JsCgVariableConstructor.kt @@ -1,8 +1,8 @@ package framework.codegen.model.constructor.tree import org.utbot.framework.codegen.model.constructor.context.CgContext +import org.utbot.framework.codegen.model.constructor.tree.CgTestClassConstructor import org.utbot.framework.codegen.model.constructor.tree.CgVariableConstructor -import org.utbot.framework.codegen.model.constructor.util.CgComponents import org.utbot.framework.codegen.model.tree.CgLiteral import org.utbot.framework.codegen.model.tree.CgValue import org.utbot.framework.codegen.model.util.nullLiteral @@ -15,7 +15,7 @@ import org.utbot.framework.plugin.api.js.JsPrimitiveModel class JsCgVariableConstructor(ctx: CgContext) : CgVariableConstructor(ctx) { - private val nameGenerator = CgComponents.getNameGeneratorBy(ctx) + private val nameGenerator = CgTestClassConstructor.CgComponents.getNameGeneratorBy(ctx) override fun getOrCreateVariable(model: UtModel, name: String?): CgValue { val baseName = name ?: nameGenerator.nameFrom(model.classId) diff --git a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt index 40500ca8ba..3d9a6db151 100644 --- a/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt +++ b/utbot-js/src/main/kotlin/framework/codegen/model/constructor/visitor/CgJsRenderer.kt @@ -5,49 +5,16 @@ import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport import org.utbot.framework.codegen.isLanguageKeyword import org.utbot.framework.codegen.model.constructor.context.CgContext -import org.utbot.framework.codegen.model.tree.CgAllocateArray -import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray -import org.utbot.framework.codegen.model.tree.CgAnonymousFunction -import org.utbot.framework.codegen.model.tree.CgArrayAnnotationArgument -import org.utbot.framework.codegen.model.tree.CgArrayElementAccess -import org.utbot.framework.codegen.model.tree.CgArrayInitializer -import org.utbot.framework.codegen.model.tree.CgConstructorCall -import org.utbot.framework.codegen.model.tree.CgDeclaration -import org.utbot.framework.codegen.model.tree.CgEqualTo -import org.utbot.framework.codegen.model.tree.CgErrorTestMethod -import org.utbot.framework.codegen.model.tree.CgErrorWrapper -import org.utbot.framework.codegen.model.tree.CgExecutableCall -import org.utbot.framework.codegen.model.tree.CgExpression -import org.utbot.framework.codegen.model.tree.CgFieldAccess -import org.utbot.framework.codegen.model.tree.CgForLoop -import org.utbot.framework.codegen.model.tree.CgGetJavaClass -import org.utbot.framework.codegen.model.tree.CgGetKotlinClass -import org.utbot.framework.codegen.model.tree.CgGetLength -import org.utbot.framework.codegen.model.tree.CgInnerBlock -import org.utbot.framework.codegen.model.tree.CgLiteral -import org.utbot.framework.codegen.model.tree.CgMethod -import org.utbot.framework.codegen.model.tree.CgMethodCall -import org.utbot.framework.codegen.model.tree.CgMultipleArgsAnnotation -import org.utbot.framework.codegen.model.tree.CgNamedAnnotationArgument -import org.utbot.framework.codegen.model.tree.CgNotNullAssertion -import org.utbot.framework.codegen.model.tree.CgParameterDeclaration -import org.utbot.framework.codegen.model.tree.CgParameterizedTestDataProviderMethod -import org.utbot.framework.codegen.model.tree.CgSpread -import org.utbot.framework.codegen.model.tree.CgStaticsRegion -import org.utbot.framework.codegen.model.tree.CgSwitchCase -import org.utbot.framework.codegen.model.tree.CgSwitchCaseLabel -import org.utbot.framework.codegen.model.tree.CgTestClass -import org.utbot.framework.codegen.model.tree.CgTestClassFile -import org.utbot.framework.codegen.model.tree.CgTestMethod -import org.utbot.framework.codegen.model.tree.CgThrowStatement -import org.utbot.framework.codegen.model.tree.CgTypeCast -import org.utbot.framework.codegen.model.tree.CgVariable +import org.utbot.framework.codegen.model.tree.* import org.utbot.framework.codegen.model.util.CgPrinter import org.utbot.framework.codegen.model.util.CgPrinterImpl import org.utbot.framework.codegen.model.visitor.CgAbstractRenderer +import org.utbot.framework.codegen.model.visitor.CgRendererContext import org.utbot.framework.plugin.api.BuiltinMethodId +import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.TypeParameters +import org.utbot.framework.plugin.api.util.isStatic import settings.JsTestGenerationSettings.fileUnderTestAliases internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgPrinterImpl()) : @@ -196,13 +163,12 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP element.values.renderElements(elementsInLine) } - @Suppress("DuplicatedCode") override fun visit(element: CgTestClassFile) { - context.collectedImports.filterIsInstance().forEach { + element.imports.filterIsInstance().forEach { renderRegularImport(it) } println() - element.testClass.accept(this) + element.declaredClass.accept(this) } override fun visit(element: CgSwitchCaseLabel) { @@ -283,6 +249,23 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP // TODO: Should we render throw statement right here? } + override fun visit(element: AbstractCgClass<*>) { + TODO("Not yet implemented") + } + + override fun visit(element: CgTestClassBody) { + // render regions for test methods + for ((i, region) in (element.testMethodRegions + element.nestedClassRegions).withIndex()) { + if (i != 0) println() + + region.accept(this) + } + + if (element.staticDeclarationRegions.isEmpty()) { + return + } + } + override fun renderMethodSignature(element: CgParameterizedTestDataProviderMethod) { throw UnsupportedOperationException() } @@ -377,6 +360,14 @@ internal class CgJsRenderer(context: CgRendererContext, printer: CgPrinter = CgP override fun escapeNamePossibleKeywordImpl(s: String): String = if (isLanguageKeyword(s, context.codeGenLanguage)) "`$s`" else s + override fun renderClassVisibility(classId: ClassId) { + TODO("Not yet implemented") + } + + override fun renderClassModality(aClass: AbstractCgClass<*>) { + TODO("Not yet implemented") + } + //TODO MINOR: check override fun String.escapeCharacters(): String = StringEscapeUtils.escapeJava(this) diff --git a/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt index 3b87fbfeb6..59a246e8b3 100644 --- a/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt +++ b/utbot-js/src/main/kotlin/fuzzer/providers/JsObjectModelProvider.kt @@ -3,7 +3,6 @@ package fuzzer.providers import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.UtAssembleModel import org.utbot.framework.plugin.api.UtExecutableCallModel -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.js.JsClassId import org.utbot.framework.plugin.api.js.JsConstructorId import org.utbot.framework.plugin.api.js.util.isJsBasic @@ -51,16 +50,13 @@ object JsObjectModelProvider : ModelProvider { } private fun assemble(id: Int, constructor: ConstructorId, values: List): FuzzedValue { - val instantiationChain = mutableListOf() + val instantiationCall = UtExecutableCallModel(null, constructor, values.map { it.model }) val model = UtAssembleModel( id, constructor.classId, "${constructor.classId.name}${constructor.parameters}#" + id.toString(16), - instantiationChain = instantiationChain, - modificationsChain = mutableListOf() - ).apply { - instantiationChain += UtExecutableCallModel(null, constructor, values.map { it.model }, this) - }.fuzzed { + instantiationCall = instantiationCall, + ) .fuzzed { summary = "%var% = ${constructor.classId.simpleName}(${constructor.parameters.joinToString { it.simpleName }})" } From 8a53d09f1d20561f20d3dc66f5890ad907cc856a Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 14:34:25 +0300 Subject: [PATCH 12/98] Fix PycharmUltimate code and uncomment js --- utbot-intellij-js/build.gradle.kts | 2 +- utbot-intellij/build.gradle.kts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts index 12bbb4ecf9..334a75ded7 100644 --- a/utbot-intellij-js/build.gradle.kts +++ b/utbot-intellij-js/build.gradle.kts @@ -64,7 +64,7 @@ intellij { "IC" -> jvmPlugins + pythonCommunityPlugins + androidPlugins "IU" -> jvmPlugins + pythonUltimatePlugins + jsPlugins + androidPlugins "PC" -> pythonCommunityPlugins - "PU" -> pythonUltimatePlugins // something else, JS? + "PY" -> pythonUltimatePlugins // something else, JS? else -> jvmPlugins } ) diff --git a/utbot-intellij/build.gradle.kts b/utbot-intellij/build.gradle.kts index 5af6a5c48a..74e8f7fdaa 100644 --- a/utbot-intellij/build.gradle.kts +++ b/utbot-intellij/build.gradle.kts @@ -100,6 +100,6 @@ dependencies { implementation(project(":utbot-python")) implementation(project(":utbot-intellij-python")) -// implementation(project(":utbot-js")) -// implementation(project(":utbot-intellij-js")) + implementation(project(":utbot-js")) + implementation(project(":utbot-intellij-js")) } \ No newline at end of file From 8abe0b79c4731028663ec6d75131a05899a22aaf Mon Sep 17 00:00:00 2001 From: Vyacheslav Tamarin Date: Thu, 6 Oct 2022 07:17:55 -0700 Subject: [PATCH 13/98] Fix Windows symbols problem --- build.gradle.kts | 4 ++-- .../plugin/language/python/PythonDialogProcessor.kt | 2 +- .../src/main/kotlin/org/utbot/python/PythonEvaluation.kt | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 474e9da095..ac1316eb06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -34,7 +34,7 @@ allprojects { tasks { withType { sourceCompatibility = "1.8" - targetCompatibility = "1.8" + targetCompatibility = "11" options.encoding = "UTF-8" options.compilerArgs = options.compilerArgs + "-Xlint:all" } @@ -47,7 +47,7 @@ allprojects { } compileTestKotlin { kotlinOptions { - jvmTarget = "11" + jvmTarget = "1.8" freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") allWarningsAsErrors = false } diff --git a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt index 2710143b96..f027b7822b 100644 --- a/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt +++ b/utbot-intellij-python/src/main/kotlin/org/utbot/intellij/plugin/language/python/PythonDialogProcessor.kt @@ -261,6 +261,6 @@ fun getDirectoriesForSysPath( return Pair( sources.map { it.path }.toSet(), - "${importPath}${file.name}".removeSuffix(".py").toPath().joinToString(".") + "${importPath}${file.name}".removeSuffix(".py").toPath().joinToString(".").replace("/", File.separator) ) } \ No newline at end of file diff --git a/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt index fc2a4dd591..ee6b541d48 100644 --- a/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt +++ b/utbot-python/src/main/kotlin/org/utbot/python/PythonEvaluation.kt @@ -58,7 +58,7 @@ fun startEvaluationProcess(input: EvaluationInput): EvaluationProcess { input.directoriesForSysPath, input.moduleToImport, input.additionalModulesToImport, - fileForOutput.path + fileForOutput.path.replace("\\", "\\\\") ) val fileWithCode = TemporaryFileManager.createTemporaryFile( runCode, @@ -78,10 +78,10 @@ fun getEvaluationResult(input: EvaluationInput, process: EvaluationProcess, time if (result.exitValue != 0) return EvaluationError( - if (result.terminatedByTimeout) "Timeout" else "Non-zero exit status" + if (result.terminatedByTimeout) "Timeout" else "Non-zero exit status: ${result.stderr}" ) - val output = process.fileForOutput.readText().split('\n') + val output = process.fileForOutput.readText().split(System.lineSeparator()) process.fileForOutput.delete() if (output.size != 4) From 542aeaed6930191db9e33d14f526bb7a5d0fbe52 Mon Sep 17 00:00:00 2001 From: Denis Fokin Date: Thu, 6 Oct 2022 16:02:56 +0300 Subject: [PATCH 14/98] Disable runIde tasks in modules where we need only Idea sdk dependencies --- utbot-intellij-js/build.gradle.kts | 1 + utbot-intellij-python/build.gradle.kts | 1 + utbot-ui-commons/build.gradle.kts | 1 + 3 files changed, 3 insertions(+) diff --git a/utbot-intellij-js/build.gradle.kts b/utbot-intellij-js/build.gradle.kts index 334a75ded7..19174b534d 100644 --- a/utbot-intellij-js/build.gradle.kts +++ b/utbot-intellij-js/build.gradle.kts @@ -9,6 +9,7 @@ val pythonUltimatePluginVersion: String? by rootProject plugins { id("org.jetbrains.intellij") version "1.7.0" } +project.tasks.asMap["runIde"]?.enabled = false tasks { compileKotlin { diff --git a/utbot-intellij-python/build.gradle.kts b/utbot-intellij-python/build.gradle.kts index 7e69443526..7e54429c3f 100644 --- a/utbot-intellij-python/build.gradle.kts +++ b/utbot-intellij-python/build.gradle.kts @@ -9,6 +9,7 @@ val pythonUltimatePluginVersion: String? by rootProject plugins { id("org.jetbrains.intellij") version "1.7.0" } +project.tasks.asMap["runIde"]?.enabled = false tasks { compileKotlin { diff --git a/utbot-ui-commons/build.gradle.kts b/utbot-ui-commons/build.gradle.kts index 54031e9083..15445e9764 100644 --- a/utbot-ui-commons/build.gradle.kts +++ b/utbot-ui-commons/build.gradle.kts @@ -4,6 +4,7 @@ val ideType: String by rootProject plugins { id("org.jetbrains.intellij") version "1.7.0" } +project.tasks.asMap["runIde"]?.enabled = false intellij { version.set("212.5712.43") From ee4519142a6d879fe8abd2a198f1f2786a65cb56 Mon Sep 17 00:00:00 2001 From: GlebSolovev Date: Wed, 5 Oct 2022 00:37:02 +0300 Subject: [PATCH 15/98] Merge actual UTBot Go, set up and fix its CLI --- settings.gradle | 5 +- utbot-cli-go/build.gradle | 86 + .../kotlin/org/utbot/cli/go/Application.kt | 36 + .../cli/go/commands/CoverageJsonStructs.kt | 13 + .../cli/go/commands/GenerateGoTestsCommand.kt | 93 + .../cli/go/commands/RunGoTestsCommand.kt | 223 + .../logic/CliGoUtTestsGenerationController.kt | 119 + .../kotlin/org/utbot/cli/go/util/FileUtils.kt | 14 + .../kotlin/org/utbot/cli/go/util/IoUtils.kt | 12 + .../cli/go/util/ProcessExecutionUtils.kt | 33 + .../org/utbot/cli/go/util/TimeMeasureUtils.kt | 8 + utbot-cli-go/src/main/resources/log4j2.xml | 16 + .../src/main/resources/version.properties | 2 + utbot-go/README.md | 111 +- utbot-go/build.gradle | 2 +- utbot-go/build.gradle.kts | 35 + utbot-go/docs/DEVELOPERS_GUIDE.md | 172 +- utbot-go/docs/diagrams/api-classes.png | Bin 0 -> 169410 bytes utbot-go/docs/diagrams/how-it-works.png | Bin 0 -> 27244 bytes .../intellij-plugin-primitive-types-demo.gif | Bin 0 -> 6742252 bytes utbot-go/go-samples/go.mod | 10 + utbot-go/go-samples/go.sum | 14 + .../simple}/primitive_types.go | 23 +- .../simple/primitive_types_go_ut_test.go | 13416 ++++++++++++++++ .../go-samples/simple/reports/coverage.html | 384 + .../go-samples/simple/reports/coverage.json | 819 + .../simple/reports/func-coverage.txt | 18 + .../go-samples/simple/reports/tests-run.txt | 4492 ++++++ .../utbot/go/api/GoUtExecutionResultsApi.kt | 4 + .../org/utbot/go/api/util/GoTypesApiUtil.kt | 2 - .../go/executor/GoFuzzedFunctionsExecutor.kt | 26 +- ...edFunctionsExecutorCodeGenerationHelper.kt | 86 +- .../utbot/go/executor/RawExecutionResults.kt | 5 +- .../kotlin/org/utbot/go/fuzzer/GoFuzzer.kt | 4 - .../providers/GoConstantsModelProvider.kt | 58 - .../GoStringConstantModelProvider.kt | 54 - .../AbstractGoUtTestsGenerationController.kt | 16 +- .../utbot/go/logic/GoTestCasesGenerator.kt | 6 +- .../go/logic/GoUtTestsGenerationConfig.kt | 18 + .../GoTestCasesCodeGenerator.kt | 20 +- .../main/kotlin/org/utbot/go/util/JsonUtil.kt | 4 +- .../org/utbot/go/util/ProcessExecutionUtil.kt | 18 +- 42 files changed, 20268 insertions(+), 209 deletions(-) create mode 100644 utbot-cli-go/build.gradle create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt create mode 100644 utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt create mode 100644 utbot-cli-go/src/main/resources/log4j2.xml create mode 100644 utbot-cli-go/src/main/resources/version.properties create mode 100644 utbot-go/build.gradle.kts create mode 100644 utbot-go/docs/diagrams/api-classes.png create mode 100644 utbot-go/docs/diagrams/how-it-works.png create mode 100644 utbot-go/docs/images/intellij-plugin-primitive-types-demo.gif create mode 100644 utbot-go/go-samples/go.mod create mode 100644 utbot-go/go-samples/go.sum rename utbot-go/{samples => go-samples/simple}/primitive_types.go (93%) create mode 100644 utbot-go/go-samples/simple/primitive_types_go_ut_test.go create mode 100644 utbot-go/go-samples/simple/reports/coverage.html create mode 100644 utbot-go/go-samples/simple/reports/coverage.json create mode 100644 utbot-go/go-samples/simple/reports/func-coverage.txt create mode 100644 utbot-go/go-samples/simple/reports/tests-run.txt delete mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoConstantsModelProvider.kt delete mode 100644 utbot-go/src/main/kotlin/org/utbot/go/fuzzer/providers/GoStringConstantModelProvider.kt create mode 100644 utbot-go/src/main/kotlin/org/utbot/go/logic/GoUtTestsGenerationConfig.kt diff --git a/settings.gradle b/settings.gradle index d0578ffc3b..9aa6282351 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,7 +23,7 @@ include 'utbot-fuzzers' include 'utbot-cli' include 'utbot-cli-python' include 'utbot-cli-js' - +include 'utbot-cli-go' include 'utbot-api' include 'utbot-instrumentation' //include 'utbot-instrumentation-tests' @@ -37,7 +37,8 @@ include 'utbot-rd' include 'utbot-python' include 'utbot-js' -//include 'utbot-go' +include 'utbot-go' + include 'utbot-ui-commons' include 'utbot-intellij-python' diff --git a/utbot-cli-go/build.gradle b/utbot-cli-go/build.gradle new file mode 100644 index 0000000000..c613b41117 --- /dev/null +++ b/utbot-cli-go/build.gradle @@ -0,0 +1,86 @@ +compileKotlin { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs += ["-Xallow-result-return-type", "-Xsam-conversions=class"] + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 +} + + +//noinspection GroovyAssignabilityCheck +configurations { + fetchInstrumentationJar +} + +dependencies { + implementation project(':utbot-framework-api') + implementation project(':utbot-framework') + implementation project(':utbot-summary') + implementation project(':utbot-cli') + implementation project(':utbot-go') + + // Without this dependency testng tests do not run. + implementation group: 'com.beust', name: 'jcommander', version: '1.48' + implementation group: 'org.junit.platform', name: 'junit-platform-console-standalone', version: junit4_platform_version + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'com.github.ajalt.clikt', name: 'clikt', version: clikt_version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: junit5Version + implementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: junit5Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2Version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2Version + implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacocoVersion + //noinspection GroovyAssignabilityCheck + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') + + implementation 'com.beust:klaxon:5.5' // to read and write JSON +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } +} + +task createProperties(dependsOn: processResources) { + doLast { + new File("$buildDir/resources/main/version.properties").withWriter { w -> + Properties properties = new Properties() + //noinspection GroovyAssignabilityCheck + properties['version'] = project.version.toString() + properties.store w, null + } + } +} + +classes { + dependsOn createProperties +} + +jar { + dependsOn project(':utbot-framework').tasks.jar + dependsOn project(':utbot-summary').tasks.jar + dependsOn project(':utbot-go').tasks.jar + dependsOn project(':utbot-fuzzers').tasks.jar + + manifest { + attributes 'Main-Class': 'org.utbot.cli.go.ApplicationKt' + attributes 'Bundle-SymbolicName': 'org.utbot.cli.go' + attributes 'Bundle-Version': "${project.version}" + attributes 'Implementation-Title': 'UtBot Go CLI' + attributes 'JAR-Type': 'Fat JAR' + } + + archiveVersion.set(project.version as String) + + dependsOn configurations.runtimeClasspath + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt new file mode 100644 index 0000000000..2492442c23 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/Application.kt @@ -0,0 +1,36 @@ +package org.utbot.cli.go + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.core.subcommands +import com.github.ajalt.clikt.parameters.options.default +import com.github.ajalt.clikt.parameters.options.option +import com.github.ajalt.clikt.parameters.options.versionOption +import com.github.ajalt.clikt.parameters.types.enum +import org.slf4j.event.Level +import org.utbot.cli.getVersion +import org.utbot.cli.setVerbosity +import kotlin.system.exitProcess +import org.utbot.cli.go.commands.GenerateGoTestsCommand +import org.utbot.cli.go.commands.RunGoTestsCommand + +class UtBotCli : CliktCommand(name = "UnitTestBot Go Command Line Interface") { + private val verbosity by option("--verbosity", help = "Changes verbosity level, case insensitive") + .enum(ignoreCase = true) + .default(Level.INFO) + + override fun run() = setVerbosity(verbosity) + + init { + versionOption(getVersion()) + } +} + +fun main(args: Array) = try { + UtBotCli().subcommands( + GenerateGoTestsCommand(), + RunGoTestsCommand(), + ).main(args) +} catch (ex: Throwable) { + ex.printStackTrace() + exitProcess(1) +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt new file mode 100644 index 0000000000..55c97cb554 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/CoverageJsonStructs.kt @@ -0,0 +1,13 @@ +package org.utbot.cli.go.commands + +import com.beust.klaxon.Json + +internal data class Position(@Json(index = 1) val line: Int, @Json(index = 2) val column: Int) + +internal data class CodeRegion(@Json(index = 1) val start: Position, @Json(index = 2) val end: Position) + +internal data class CoveredSourceFile( + @Json(index = 1) val sourceFileName: String, + @Json(index = 2) val covered: List, + @Json(index = 3) val uncovered: List +) \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt new file mode 100644 index 0000000000..7724a72aa9 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/GenerateGoTestsCommand.kt @@ -0,0 +1,93 @@ +package org.utbot.cli.go.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.long +import mu.KotlinLogging +import org.utbot.cli.go.logic.CliGoUtTestsGenerationController +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.cli.go.util.toAbsolutePath +import org.utbot.go.logic.GoUtTestsGenerationConfig +import java.nio.file.Files +import java.nio.file.Paths + +private val logger = KotlinLogging.logger {} + +class GenerateGoTestsCommand : + CliktCommand(name = "generateGo", help = "Generates tests for the specified Go source file") { + + private val sourceFile: String by option( + "-s", "--source", + help = "Specifies Go source file to generate tests for" + ) + .required() + .check("Must exist and ends with *.go suffix") { + it.endsWith(".go") && Files.exists(Paths.get(it)) + } + + private val selectedFunctionsNames: List by option( + "-f", "--function", + help = StringBuilder() + .append("Specifies function name to generate tests for. ") + .append("Can be used multiple times to select multiple functions at the same time.") + .append("If no functions are specified, all functions contained in the source file are selected") + .toString() + ) + .multiple() + + private val goExecutablePath: String by option( + "-go", "--go-path", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val eachFunctionExecutionTimeoutMillis: Long by option( + "-t", "--each-execution-timeout", + help = StringBuilder() + .append("Specifies a timeout in milliseconds for each fuzzed function execution.") + .append("Default is ${GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS} ms") + .toString() + ) + .long() + .default(GoUtTestsGenerationConfig.DEFAULT_EACH_EXECUTION_TIMEOUT_MILLIS) + .check("Must be positive") { it > 0 } + + private val printToStdOut: Boolean by option( + "-p", + "--print-test", + help = "Specifies whether a test should be printed out to StdOut. Is disabled by default" + ) + .flag(default = false) + + private val overwriteTestFiles: Boolean by option( + "-w", + "--overwrite", + help = "Specifies whether to overwrite the output test file if it already exists. Is disabled by default" + ) + .flag(default = false) + + override fun run() { + val sourceFileAbsolutePath = sourceFile.toAbsolutePath() + val goExecutableAbsolutePath = goExecutablePath.toAbsolutePath() + + val testsGenerationStarted = now() + logger.debug { "Generating test for [$sourceFile] - started" } + try { + CliGoUtTestsGenerationController( + cliLogger = logger, + printToStdOut = printToStdOut, + overwriteTestFiles = overwriteTestFiles + ).generateTests( + mapOf(sourceFileAbsolutePath to selectedFunctionsNames), + GoUtTestsGenerationConfig(goExecutableAbsolutePath, eachFunctionExecutionTimeoutMillis) + ) + } catch (t: Throwable) { + logger.error { "An error has occurred while generating test for snippet $sourceFile: $t" } + throw t + } finally { + val duration = durationInMillis(testsGenerationStarted) + logger.debug { "Generating test for [$sourceFile] - completed in [$duration] (ms)" } + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt new file mode 100644 index 0000000000..f8a5773655 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/commands/RunGoTestsCommand.kt @@ -0,0 +1,223 @@ +package org.utbot.cli.go.commands + +import com.github.ajalt.clikt.core.CliktCommand +import com.github.ajalt.clikt.parameters.options.* +import com.github.ajalt.clikt.parameters.types.choice +import mu.KotlinLogging +import org.utbot.cli.go.util.* +import org.utbot.go.util.convertObjectToJsonString +import java.io.File + +private val logger = KotlinLogging.logger {} + +class RunGoTestsCommand : CliktCommand(name = "runGo", help = "Runs tests for the specified Go package") { + + private val packageDirectory: String by option( + "-p", "--package", + help = "Specifies Go package to run tests for" + ) + .required() + .check("Must exist and be directory") { + File(it).let { file -> file.exists() && file.isDirectory } + } + + private val goExecutablePath: String by option( + "-go", "--go-path", + help = "Specifies path to Go executable. For example, it could be [/usr/local/go/bin/go] for some systems" + ) + .required() // TODO: attempt to find it if not specified + + private val verbose: Boolean by option( + "-v", "--verbose", + help = "Specifies whether an output should be verbose. Is disabled by default" + ) + .flag(default = false) + + private val json: Boolean by option( + "-j", "--json", + help = "Specifies whether an output should be in JSON format. Is disabled by default" + ) + .flag(default = false) + + private val output: String? by option( + "-o", "--output", + help = "Specifies output file for tests run report. Prints to StdOut by default" + ) + + private enum class CoverageMode(val displayName: String) { + REGIONS_HTML("html"), PERCENTS_BY_FUNCS("func"), REGIONS_JSON("json"); + + override fun toString(): String = displayName + + val fileExtensionValidator: (String) -> Boolean + get() = when (this) { + REGIONS_HTML -> { + { it.substringAfterLast('.') == "html" } + } + + REGIONS_JSON -> { + { it.substringAfterLast('.') == "json" } + } + + PERCENTS_BY_FUNCS -> { + { true } + } + } + } + + private val coverageMode: CoverageMode? by option( + "-cov-mode", "--coverage-mode", + help = StringBuilder() + .append("Specifies whether a test coverage report should be generated and defines its mode. ") + .append("Coverage report generation is disabled by default") + .toString() + ) + .choice( + CoverageMode.REGIONS_HTML.toString() to CoverageMode.REGIONS_HTML, + CoverageMode.PERCENTS_BY_FUNCS.toString() to CoverageMode.PERCENTS_BY_FUNCS, + CoverageMode.REGIONS_JSON.toString() to CoverageMode.REGIONS_JSON, + ) + .check( + StringBuilder() + .append("Test coverage report output file must be set ") + .append("and have an extension that matches the coverage mode") + .toString() + ) { mode -> + coverageOutput?.let { mode.fileExtensionValidator(it) } ?: false + } + + private val coverageOutput: String? by option( + "-cov-out", "--coverage-output", + help = "Specifies output file for test coverage report. Required if [--coverage-mode] is set" + ) + .check("Test coverage report mode must be specified") { + coverageMode != null + } + + override fun run() { + val runningTestsStarted = now() + try { + logger.debug { "Running tests for [$packageDirectory] - started" } + + /* run tests */ + + val packageDirectoryFile = File(packageDirectory).canonicalFile + + val coverProfileFile = if (coverageMode != null) { + createFile(createCoverProfileFileName()) + } else { + null + } + + try { + val runGoTestCommand = mutableListOf( + goExecutablePath.toAbsolutePath(), + "test", + "./" + ) + if (verbose) { + runGoTestCommand.add("-v") + } + if (json) { + runGoTestCommand.add("-json") + } + if (coverageMode != null) { + runGoTestCommand.add("-coverprofile") + runGoTestCommand.add(coverProfileFile!!.canonicalPath) + } + + val outputStream = if (output == null) { + System.out + } else { + createFile(output!!).outputStream() + } + executeCommandAndRedirectStdoutOrFail(runGoTestCommand, packageDirectoryFile, outputStream) + + /* generate coverage report */ + + val coverageOutputFile = coverageOutput?.let { createFile(it) } ?: return + + when (coverageMode) { + null -> { + return + } + + CoverageMode.REGIONS_HTML, CoverageMode.PERCENTS_BY_FUNCS -> { + val runToolCoverCommand = mutableListOf( + "go", + "tool", + "cover", + "-${coverageMode!!.displayName}", + coverProfileFile!!.canonicalPath, + "-o", + coverageOutputFile.canonicalPath + ) + executeCommandAndRedirectStdoutOrFail(runToolCoverCommand, packageDirectoryFile) + } + + CoverageMode.REGIONS_JSON -> { + val coveredSourceFiles = parseCoverProfile(coverProfileFile!!) + val jsonCoverage = convertObjectToJsonString(coveredSourceFiles) + coverageOutputFile.writeText(jsonCoverage) + } + } + } finally { + coverProfileFile?.delete() + } + } catch (t: Throwable) { + logger.error { "An error has occurred while running tests for [$packageDirectory]: $t" } + throw t + } finally { + val duration = durationInMillis(runningTestsStarted) + logger.debug { "Running tests for [$packageDirectory] - completed in [$duration] (ms)" } + } + } + + private fun createCoverProfileFileName(): String { + return "ut_go_cover_profile.out" + } + + private fun parseCoverProfile(coverProfileFile: File): List { + data class CoverageRegions( + val covered: MutableList, + val uncovered: MutableList + ) + + val coverageRegionsBySourceFilesNames = mutableMapOf() + + coverProfileFile.readLines().asSequence() + .drop(1) // drop "mode" value + .forEach { fullLine -> + val (sourceFileFullName, coverageInfoLine) = fullLine.split(":", limit = 2) + val sourceFileName = sourceFileFullName.substringAfterLast("/") + val (regionString, _, countString) = coverageInfoLine.split(" ", limit = 3) + + fun parsePosition(positionString: String): Position { + val (lineNumber, columnNumber) = positionString.split(".", limit = 2).asSequence() + .map { it.toInt() } + .toList() + return Position(lineNumber, columnNumber) + } + val (startString, endString) = regionString.split(",", limit = 2) + val region = CodeRegion(parsePosition(startString), parsePosition(endString)) + + val regions = coverageRegionsBySourceFilesNames.getOrPut(sourceFileName) { + CoverageRegions( + mutableListOf(), + mutableListOf() + ) + } + // it is called "count" in docs, but in reality it is like boolean for covered / uncovered + val count = countString.toInt() + if (count == 0) { + regions.uncovered.add(region) + } else { + regions.covered.add(region) + } + } + + return coverageRegionsBySourceFilesNames.map { (sourceFileName, regions) -> + CoveredSourceFile(sourceFileName, regions.covered, regions.uncovered) + } + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt new file mode 100644 index 0000000000..252c914950 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/logic/CliGoUtTestsGenerationController.kt @@ -0,0 +1,119 @@ +package org.utbot.cli.go.logic + +import mu.KLogger +import org.utbot.cli.go.util.durationInMillis +import org.utbot.cli.go.util.now +import org.utbot.go.api.GoUtFile +import org.utbot.go.api.GoUtFunction +import org.utbot.go.api.GoUtFuzzedFunctionTestCase +import org.utbot.go.gocodeanalyzer.GoSourceCodeAnalyzer +import org.utbot.go.logic.AbstractGoUtTestsGenerationController +import java.io.File +import java.time.LocalDateTime + +class CliGoUtTestsGenerationController( + private val cliLogger: KLogger, + private val printToStdOut: Boolean, + private val overwriteTestFiles: Boolean +) : AbstractGoUtTestsGenerationController() { + + private lateinit var currentStageStarted: LocalDateTime + + override fun onSourceCodeAnalysisStart(targetFunctionsNamesBySourceFiles: Map>): Boolean { + currentStageStarted = now() + cliLogger.debug { "Source code analysis - started" } + + return true + } + + override fun onSourceCodeAnalysisFinished( + analysisResults: Map + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + cliLogger.debug { "Source code analysis - completed in [$stageDuration] (ms)" } + + return handleMissingSelectedFunctions(analysisResults) + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsStart( + sourceFile: GoUtFile, + functions: List + ): Boolean { + currentStageStarted = now() + cliLogger.debug { "Generating test for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesGenerationForGoSourceFileFunctionsFinished( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + val stageDuration = durationInMillis(currentStageStarted) + cliLogger.debug { + "Test cases generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + override fun onTestCasesFileCodeGenerationStart( + sourceFile: GoUtFile, + testCases: List + ): Boolean { + currentStageStarted = now() + cliLogger.debug { "Test cases file code generation for [${sourceFile.fileName}] - started" } + + return true + } + + override fun onTestCasesFileCodeGenerationFinished(sourceFile: GoUtFile, generatedTestsFileCode: String): Boolean { + if (printToStdOut) { + cliLogger.info { generatedTestsFileCode } + return true + } + writeGeneratedCodeToFile(sourceFile, generatedTestsFileCode) + + val stageDuration = durationInMillis(currentStageStarted) + cliLogger.debug { + "Test cases file code generation for [${sourceFile.fileName}] functions - completed in [$stageDuration] (ms)" + } + + return true + } + + private fun handleMissingSelectedFunctions( + analysisResults: Map + ): Boolean { + val missingSelectedFunctionsListMessage = generateMissingSelectedFunctionsListMessage(analysisResults) + val okSelectedFunctionsArePresent = + analysisResults.any { (_, analysisResult) -> analysisResult.functions.isNotEmpty() } + + if (missingSelectedFunctionsListMessage != null) { + cliLogger.warn { "Some selected functions were skipped during source code analysis.$missingSelectedFunctionsListMessage" } + } + if (!okSelectedFunctionsArePresent) { + throw Exception("Nothing to process. No functions were provided") + } + + return true + } + + private fun writeGeneratedCodeToFile(sourceFile: GoUtFile, generatedTestsFileCode: String) { + val testsFileNameWithExtension = createTestsFileNameWithExtension(sourceFile) + val testFile = File(sourceFile.absoluteDirectoryPath).resolve(testsFileNameWithExtension) + if (testFile.exists()) { + val alreadyExistsMessage = "File [${testFile.absolutePath}] already exists" + if (overwriteTestFiles) { + cliLogger.warn { "$alreadyExistsMessage: it will be overwritten" } + } else { + cliLogger.warn { "$alreadyExistsMessage: skipping test generation for [${sourceFile.fileName}]" } + return + } + } + testFile.writeText(generatedTestsFileCode) + } + + private fun createTestsFileNameWithExtension(sourceFile: GoUtFile) = + sourceFile.fileNameWithoutExtension + "_go_ut_test.go" +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt new file mode 100644 index 0000000000..2c4540b090 --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/FileUtils.kt @@ -0,0 +1,14 @@ +package org.utbot.cli.go.util + +import java.io.File + +fun String.toAbsolutePath(): String = File(this).canonicalPath + +fun createFile(filePath: String): File = createFile(File(filePath).canonicalFile) + +fun createFile(file: File): File { + return file.also { + it.parentFile?.mkdirs() + it.createNewFile() + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt new file mode 100644 index 0000000000..220b2f0e5f --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/IoUtils.kt @@ -0,0 +1,12 @@ +package org.utbot.cli.go.util + +import java.io.InputStream +import java.io.OutputStream + +fun copy(from: InputStream, to: OutputStream?) { + val buffer = ByteArray(10240) + var len: Int + while (from.read(buffer).also { len = it } != -1) { + to?.write(buffer, 0, len) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt new file mode 100644 index 0000000000..3dbeebd99b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/ProcessExecutionUtils.kt @@ -0,0 +1,33 @@ +package org.utbot.cli.go.util + +import java.io.File +import java.io.InputStreamReader +import java.io.OutputStream + +fun executeCommandAndRedirectStdoutOrFail( + command: List, + workingDirectory: File? = null, + redirectStdoutToStream: OutputStream? = null // if null, stdout of process is suppressed +) { + val executedProcess = runCatching { + val process = ProcessBuilder(command) + .redirectOutput(ProcessBuilder.Redirect.PIPE) + .redirectErrorStream(true) + .directory(workingDirectory) + .start() + copy(process.inputStream, redirectStdoutToStream) + process.waitFor() + process + }.getOrElse { + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with throwable: $it" + ) + } + val exitCode = executedProcess.exitValue() + if (exitCode != 0) { + val processOutput = InputStreamReader(executedProcess.inputStream).readText() + throw RuntimeException( + "Execution of [${command.joinToString(separator = " ")}] failed with non-zero exit code = $exitCode:\n$processOutput" + ) + } +} \ No newline at end of file diff --git a/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt new file mode 100644 index 0000000000..c72601574b --- /dev/null +++ b/utbot-cli-go/src/main/kotlin/org/utbot/cli/go/util/TimeMeasureUtils.kt @@ -0,0 +1,8 @@ +package org.utbot.cli.go.util + +import java.time.LocalDateTime +import java.time.temporal.ChronoUnit + +fun now(): LocalDateTime = LocalDateTime.now() + +fun durationInMillis(started: LocalDateTime): Long = ChronoUnit.MILLIS.between(started, now()) \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/log4j2.xml b/utbot-cli-go/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..3d6ee82bcf --- /dev/null +++ b/utbot-cli-go/src/main/resources/log4j2.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/utbot-cli-go/src/main/resources/version.properties b/utbot-cli-go/src/main/resources/version.properties new file mode 100644 index 0000000000..956d6e337a --- /dev/null +++ b/utbot-cli-go/src/main/resources/version.properties @@ -0,0 +1,2 @@ +#to be populated during the build task +version=N/A \ No newline at end of file diff --git a/utbot-go/README.md b/utbot-go/README.md index d7071bd3ff..fbad639cd2 100644 --- a/utbot-go/README.md +++ b/utbot-go/README.md @@ -34,7 +34,56 @@ Function result types are supported the same as for parameters, but with _suppor In addition, UTBot Go correctly captures not only errors returned by functions, but also _`panic` cases_. -Examples of supported functions can be found [here](samples). +Examples of supported functions can be found [here](go-samples/simple/primitive_types.go). + +## Important notes + +### Where are tests generated? + +It is true that in the described API it is currently almost impossible to customize the file in which the tests are +generated. By default, test generation results in the file `[name of source file]_go_ut_test.go` _located in the same +directory and Go package_ as the source file. + +In other words, tests are generated right next to the source code. But why? + +* Go was created for convenient and fast development, therefore it has appropriate guidelines: `Testing code typically + lives in the same package as the code it tests` ([source](https://gobyexample.com/testing)). For example, this + approach provides a clear file structure and allows you to run tests as simply and quickly as possible. +* Placing tests in the same package with the source code allows you to test private functions. Yes, this is not good + practice in programming in general: but, again, it allows you to develop in Go faster by automatically checking even + the internal implementation of the public API of the package via unit testing. +* This approach avoids problems with dependencies from imported packages etc. It's always nice not to have them, if + possible. + +Of course, Go has the ability to store tests away from the source code. In the future, it is planned to support this +functionality in the UTBot Go. + +However, the word `almost` in the first sentence of this section is not redundant at all, there is _a small hack_. When +using the `generateGo` CLI command, you can set the generated tests output mode to StdOut (`-p, --print-test` flag). +Then using, for example, bash primitives, you can redirect the output to an arbitrary file. Such a solution will not +solve possible problems with dependencies, but will automatically save the result of the generation in the right place. + +### Is there any specific structure of Go source files required? + +Yes, unfortunately or fortunately, it is required. Namely, the source code file for which the tests are generated _must +be in a Go project_ consisting of a module and packages. + +But do not be afraid! Go is designed for convenient and fast development, so _it's easy to start a Go +project_. For example, the [starter tutorial](https://go.dev/doc/tutorial/getting-started) of the language just +tells how to create the simplest project in Go. For larger projects, it is recommended to read a couple of sections of +the tutorial further: [Create a Go module](https://go.dev/doc/tutorial/create-module) +and [Call your code from another module](https://go.dev/doc/tutorial/call-module-code). + +To put it simply and briefly, in the simplest case, it is enough to use one call to the `go mod init` command. For more +complex ones, `go mod tidy` and `go mod edit` may come in handy. Finally, when developing in IntelliJ IDEA, you almost +don’t have to think about setting up a project: it will set everything up by itself. + +But _why does UTBot Go need a Go project_ and not enough files in a vacuum? The answer is simple — +dependencies. Go modules are designed to conveniently support project dependencies, which are simply listed in +the `go.mod` file. Thanks to it, modern Go projects are easy to reproduce and, importantly for UTBot Go, to test. + +In the future, it is planned to add the ability to accept arbitrary code as input to UTBot Go and generate the simplest +Go project automatically. ## Install and use easily @@ -43,7 +92,7 @@ Examples of supported functions can be found [here](samples). _Requirements:_ * `IntelliJ IDEA (Ultimate Edition)`, compatible with version `2022.1`; -* installed `Go SDK` version compatible with `1.19` or `1.18`; +* installed `Go SDK` version later than `1.18`; * installed in IntelliJ IDEA [Go plugin](https://plugins.jetbrains.com/plugin/9568-go), compatible with the IDE version (it is for this that the `Ultimate` edition of the IDE is needed); * properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file @@ -56,7 +105,7 @@ Most likely, if you are already developing Go project in IntelliJ IDEA, then you * just find the latest version of [UnitTestBot](https://plugins.jetbrains.com/plugin/19445-unittestbot) in the plugin market; * or download zip archive with `utbot-intellij JAR` - from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2926264476) and install it in IntelliJ IDEA as + from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/3012565900) and install it in IntelliJ IDEA as follows from plugins section (yes, you need to select the entire downloaded zip archive, it does not need to be unpacked). ![](docs/images/install-intellij-plugin-from-disk.png) @@ -65,48 +114,68 @@ Finally, you can _start using UTBot Go_: open any `.go` file in the I that, a window will appear in which you can configure the test generation settings and start running it in a couple of clicks. -[//]: # (See some example screenshots:) - -[//]: # () - -[//]: # (* opened `.go` source code file) +See an example demo for [`primitive_types.go`](go-samples)! -[//]: # (* test generation configuration window) - -[//]: # (* generated file with tests) +![](docs/images/intellij-plugin-primitive-types-demo.gif) ### CLI application _Requirements:_ * installed `Java SDK` version `11` or higher; -* installed `Go SDK` version compatible with `1.19` or `1.18`; +* installed `Go SDK` version later than `1.18`; * properly configured Go module for source code file (i.e. for file to generate tests for): corresponding `go.mod` file must exist. _To install the UTBot Go CLI application:_ download zip archive containing `utbot-cli JAR` -from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/2926264476), then extract its content (JAR file) to a +from [here](https://github.com/UnitTestBot/UTBotJava/actions/runs/3012565900), then extract its content (JAR file) to a convenient location. -Finally, you can _start using UTBot Go_ by running the extracted JAR on the command line. For example, to -find out about all flags of UTBot Go CLI application, run the command as follows (`utbot-cli-2022.8-beta.jar` here is -the path to the extracted JAR). +Finally, you can _start using UTBot Go_ by running the extracted JAR on the command line. Two actions are +currently supported: `generateGo` and `runGo` for generating and running tests, respectively. + +For example, to find out about all options for actions, run the commands as follows +(`utbot-cli-2022.8-beta.jar` here is the path to the extracted JAR): ```bash java -jar utbot-cli-2022.8-beta.jar generateGo --help ``` -_UTBot Go CLI application options:_ +or + +```bash +java -jar utbot-cli-2022.8-beta.jar runGo --help +``` + +respectively. + +_Action `generateGo` options:_ * `-s, --source TEXT`, _required_: specifies Go source file to generate tests for. * `-f, --function TEXT`: specifies function name to generate tests for. Can be used multiple times to select multiple functions at the same time. If no functions are specified, all functions contained in the source file are selected. -* `-g, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` for - some systems. -* `-p, --print-test`: specifies whether a test should be printed out to StdOut. -* `-w, --overwrite`: specifies whether to overwrite the output test file if it already exists. +* `-go, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` + for some systems. +* `-t, --each-execution-timeout INT`: specifies a timeout in milliseconds for each fuzzed function execution. Default is + `1000` ms. +* `-p, --print-test`: specifies whether a test should be printed out to StdOut. Is disabled by default. +* `-w, --overwrite`: specifies whether to overwrite the output test file if it already exists. Is disabled by default. * `-h, --help`: show help message and exit. +_Action `runGo` options:_ + +* `-p, --package TEXT`, _required_: specifies Go package to run tests for. +* `-go, --go-path TEXT`, _required_: specifies path to Go executable. For example, it could be `/usr/local/go/bin/go` + for some systems. +* `-v, --verbose`: specifies whether an output should be verbose. Is disabled by default. +* `-j, --json`: specifies whether an output should be in JSON format. Is disabled by default. +* `-o, --output TEXT`: specifies output file for tests run report. Prints to StdOut by default. +* `-cov-mode, --coverage-mode [html|func|json]`: specifies whether a test coverage report should be generated and + defines its mode. Coverage report generation is disabled by default. Examples of different coverage reports modes can + be found [here](go-samples/simple/reports). +* `-cov-out, --coverage-output TEXT`: specifies output file for test coverage report. Required if `[--coverage-mode]` is + set. + ## Contribute to UTBot Go If you want to _take part in the development_ of the project or _learn more_ about how it works, check diff --git a/utbot-go/build.gradle b/utbot-go/build.gradle index 1ccc8e3136..a5cb89fe25 100644 --- a/utbot-go/build.gradle +++ b/utbot-go/build.gradle @@ -17,7 +17,7 @@ dependencies { testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8' - implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlinLoggingVersion + implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version implementation 'com.beust:klaxon:5.5' // to read and write JSON diff --git a/utbot-go/build.gradle.kts b/utbot-go/build.gradle.kts new file mode 100644 index 0000000000..c84fbcd511 --- /dev/null +++ b/utbot-go/build.gradle.kts @@ -0,0 +1,35 @@ +val intellijPluginVersion: String? by rootProject +val kotlinLoggingVersion: String? by rootProject +val apacheCommonsTextVersion: String? by rootProject +val jacksonVersion: String? by rootProject +val ideType: String? by rootProject +val pythonCommunityPluginVersion: String? by rootProject +val pythonUltimatePluginVersion: String? by rootProject + +tasks { + compileKotlin { + kotlinOptions { + jvmTarget = "11" + freeCompilerArgs = freeCompilerArgs + listOf("-Xallow-result-return-type", "-Xsam-conversions=class") + allWarningsAsErrors = false + } + } + + java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_11 + } + + test { + useJUnitPlatform() + } +} + +dependencies { + api(project(":utbot-fuzzers")) + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("com.beust:klaxon:5.5") + implementation(group = "io.github.microutils", name = "kotlin-logging", version = kotlinLoggingVersion) +} \ No newline at end of file diff --git a/utbot-go/docs/DEVELOPERS_GUIDE.md b/utbot-go/docs/DEVELOPERS_GUIDE.md index df3dd964e4..b2d09d6408 100644 --- a/utbot-go/docs/DEVELOPERS_GUIDE.md +++ b/utbot-go/docs/DEVELOPERS_GUIDE.md @@ -1,12 +1,176 @@ # UTBot Go: Developers Guide -## How UTBot-Go works in general +# How UTBot Go works in general -_**TODO:**_ pipeline description and scheme. +![](diagrams/how-it-works.png) -## Deep dive: project's codebase and architecture decisions +In the diagram above, you can see _the main stages of the UTBot Go test generation pipeline_. Let's take a look at each +in more detail! -_**TODO:**_ class diagrams + more detailed description + some architecture decisions. +### Targets selection and configuration + +This block in the diagram is highlighted in a separate color, since the action is mainly performed by the user. Namely, +the user selects the target source file and functions for which tests will be generated and configure generation +parameters (for example, fuzzed function execution timeout, path to Go executable, etc.). + +If UTBot Go is built as a plugin for IntelliJ IDEA, then the user opens the target Go source file and, using a keyboard +shortcut, calls up a dialog window where it's possible to select functions and configure settings. + +If UTBot Go is build as a CLI application, then the user sets the functions and parameters using command line flags. + +### Go source code analysis + +Once the user has chosen the target functions, it is necessary to collect information about them. Namely: their +signatures and information about types (in the basic version); constants in the body of functions and more (in the +future). This is exactly what happens at the stage of code analysis. As a result, UTBot Go gets an internal +representation of the target functions, sufficient for the subsequent generation of tests. + +### Fuzzing + +Fuzzing is the first part of the test cases generation process. At this stage, values, that will be used +to test the target functions, are generated. Namely, to be passed to functions as arguments in the next steps. + +For now, the most basic fuzzing technique is used: for each type a list of predefined corner-case values is generated. +For example, for `int` type: `0`, `1`, `-1`, `math.MinInt`, and `math.MaxInt`. In the future, values generation can be +improved by using constants from the body of the target functions and some randomizing. + +### Fuzzed functions execution + +In the previous step, UTBot Go generated the values that the functions need to be tested with — now the task is to +do this. Namely, execute the functions with the values generated for them and save the result. + +Essentially, the target function, the values generated for it, and the result of its execution form a test case. In +fact, that is why this stage ends the global process of generating test cases. + +### Test code generation + +Finally, the last stage: the test cases are ready, UTBot Go needs only to generate code for them. Nothing terrible +happens here, but one need to carefully consider the features of the Go language (for example, the necessity to cast +constants to the desired type, oh). + +_That's how the world (UTBot Go) works!_ + +# Deep dive: project's codebase and architecture decisions + +## Api + +![](diagrams/api-classes.png) + +Api classes are responsible for Go's internal representation of types, value models, functions, and their results. These +classes are used throughout the UTBot Go logic. + +The vast majority of classes are in the corresponding [`api` package](../src/main/kotlin/org/utbot/go/api). However, the +`GoClassId` and `GoUtModel` classes can be found in the common for +UTBot `utbot-framework-api` module, +in [`GoApi.kt`](../../utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/go/GoApi.kt) file for +compatibility reasons. Also, to ensure compatibility, Api classes are used in many `when`-s of the original UTBot code: +however, there is no logic there, only errors are thrown. + +The following sections will describe the Api classes in detail. However, Api also has util functions separately +collected in [util package](../src/main/kotlin/org/utbot/go/api/util). It is useful to study them if you need to work +with Api classes a lot and deeply (so you can reuse some of utils). + +### Types representation + +In the original UTBot, the base class of the type system is `ClassId`. Accordingly, `GoClassId` is used for +compatibility +with it. Also, the `GoSyntheticMultipleTypesId` and `GoSyntheticNoTypeId` classes are needed solely to convert +inexpressible (in terms of regular UTBot types) Go types to `GoClassId`: multiple results returned from a function and +no +return value, respectively. The only real class for all Go types is the `GoTypeId` class, which describes any Go types +in +the project. So far, this is where the type hierarchy ends, but `GoTypeId` may have descendants in the future. + +`GoTypeId` class. At the moment, the type is identified by its name, which can be found in the source Go code. All other +fields / methods are currently needed for code generation: + +* `ImplementsError` allows you to separate successful runs and those that ended with an error; +* `IsPrimitiveGoType(): Boolean` is useful for generating panic tests (if the type of panic value is primitive, then it + can be checked, otherwise not yet); +* `neverRequiresExplicitCast(): Boolean` is self-explanatory; +* `correspondingKClass(): KClass` allows you, if possible, to convert a value of a Go-defined type to a Kotlin + value, rather than storing it as a string: for now, this is just done, but in the future it may be useful. + +By the way, most of the methods are written as extensions in the utility files. + +### Value models representation + +Similar to the type hierarchy, at the source is the base class of the original UTBot's model hierarchy — +`UtModel`. Accordingly, its successor, which defines all models for Go, is the `GoUtModel` class. The `classId` field +just refers to `UtModel`, but uses the `GoClassId` specification (not `GoTypeId` — for compatibility); +the `requiredImports`field is needed for code generation (some values, for example, such as `math.MaxInt`, require +certain packages). + +The successors of `GoUtModel` are already classes that in one way or another set and store the described value. For +example, the `GoUtPrimitiveModel` class describes a primitive type value model, so it directly stores it (`value: Any` +field). The `GoUtNilModel` class, on the other hand, defines the model for the `nil` value in the Go language: +obviously, no explicit value needs to be stored in it. In the future, the list of `GoUtModel` descendants will have to +be supplemented by classes that describe the value models of structures built into Go (arrays, slices, maps, ...), +custom classes, etc. + +Learn more about the `GoUtPrimitiveModel` class. Above, only one of its fields was not mentioned +— `explicitCastMode`, which is necessary exclusively for code generation. For example, values of complex types +never need an explicit cast, as they are generated with an explicit `complex64` / `complex128` constructor. Values of +type `float32`, on the contrary, are almost always needed, since otherwise they will be converted to `float64` by +default (respectively, `REQUIRED` will be set to the model at some point). And so on. + +The `toValueGoCode(): String` and `toCastedValueGoCode(): String` methods allow you to set the display of the value in +code generation — they are automatically used in the appropriate way in `toString(): String`. + +Finally, `GoUtPrimitiveModel` has several descendants, which in turn define models with even more specific values of +primitive types. `GoUtComplexModel` specifies the value models of complex types (`complex64` and `complex128` in Go), +respectively: they need to store two floating point values, for the real and imaginary parts. `GoUtFloatInfModel` and +`GoUtFloatNaNModel` in turn define special values for floating point types: infinity and `NaN`. They cannot be expressed +correctly as a number in the form of `GoUtPrimitveModel`, so special classes solve this problem. + +### Functions representation + +The most important data classes of the entire API are `GoUtFunction`, which define the internal representation of the +tested functions. UTBot Go receives the input functions just by their names and paths to the source Go file; and then +`GoSourceCodeAnalyzer` extracts information about them from the source code exactly in the form of `GoUtFunction`-s. In +fact, `GoUtFunction` contains all the information necessary for generating tests: signature (`name`, `parameters`, +`resultTypes`), information about the source file (`sourceFile`), constants from its body (`concreteValues`). Of course, +this list of fields may be expanded in the future: for example, by the receiver field to support Go methods. + +`GoUtFunctionParameter` and `GoUtFile` are only auxiliary data classes for better organization of stored information. +What is unusual is that the `sourceFile` field in `GoUtFunction` is private, but this is done intentionally. As will be +discussed later (in the Core section), functions to be tested are passed inside UTBot Go as a map keyed by their source +files (`GoUtFile`-s): this makes possible to potentially optimize test generation. At the same time, it would be useful +to leave information about the source file inside `GoUtFunction` — at least for the sake of uniqueness; otherwise, +two identically named functions from different Go packages may be unexpectedly lost while moving them between map and +set structures. However, in order not to create ambiguities, the information about the file has been made private inside +`GoUtFunction`: the right way to access it is to use the key of the map in which this function was passed. + +Finally, `GoUtFuzzedFunction` and `GoUtFuzzedFunctionTestCase` are just convenient data classes for expressing the +appropriate objects within UTBot Go: if it is a fuzzed function or a fuzzed function bundled with a result of +its execution, respectively. + +### Execution results representation + +To represent the results of fuzzed functions execution, the `GoUtExecutionResult` class hierarchy is used. Such objects +are created in a predictable way: `GoFuzzedFunctionsExecutor` gets `GoUtFuzzedFunctions`, then it executes them and +returns information about the results exactly in the form of `GoUtExecutionResult`-s. + +`GoUtExecutionResult`-s exist in a form of a class hierarchy: it allows developers to better differentiate the types of +results; that can be convenient in code generation (for example, to divide tests in different test suites). +`GoUtExecutionResult` has three children: `GoUtExecutionCompleted` for functions that return results, `GoUtPanicFailure` +for functions that panic (in Go this is similar to throwing unchecked exceptions in Java) and `GoUtTimeoutExceeded` for +cases where the execution of the functions was too long and was interrupted by a timeout. The listed classes contain the +corresponding fields: result models, a description of the panic value (in Go, a `panic` is called with some argument, +for example, `panic("error description")`), a timeout value, respectively. + +At the same time, `GoUtExecutionCompleted`, like `GoUtExecutionResult`, is an interface since they are not final in the +hierarchy. `GoUtExecutionCompleted` has two implementations that differ only logically: `GoUtExecutionSuccess` and +`GoUtExecutionWithNonNilError`, both are self-explanatory. However, this needs to be explained: in Go, checked +errors are passed as return values of functions, so it is a common agreement to return `nil` on success and a non-nil +error object otherwise. That is why the execution of a function with non-nil errors in the results is represented by a +separate class (`GoUtExecutionWithNonNilError`), while the remaining cases are considered +successful (`GoUtExecutionSuccess`). + +Of course, in the future, if necessary, the hierarchy can be easily expanded (for example, by describing the memory +overflow case). + +_That's how the world (Api classes of UTBot Go) is designed!_ ## How to test UTBot Go diff --git a/utbot-go/docs/diagrams/api-classes.png b/utbot-go/docs/diagrams/api-classes.png new file mode 100644 index 0000000000000000000000000000000000000000..141b855f216971a64de475a23116362df9079c5a GIT binary patch literal 169410 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

f%J5Vf*5$Hjq2gJB;SAc+ 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%0Do;((}lAGs<^>Y5|Qxc*jM1XEt5v2)v(uK1UsW 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μ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