diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dfd48feacc..082931e9ac 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -43,19 +43,7 @@ public void testFail_errors() { **Environment** -Test framework: - -Java: - -Mockito: - - - [ ] Other packages - - [ ] Other classes - - [ ] Static mocking - -Test: - -- [ ] Parametrized +_Substitute this text with an information that can help us to recreate the environment. For instance, specify Java version, test framework with a version (JUnit, TestNG...), Mockito configuration (packages, classes, statics), were the test parametrized or not and so on._ **Additional context** diff --git a/.github/ISSUE_TEMPLATE/test_request.md b/.github/ISSUE_TEMPLATE/test_request.md new file mode 100644 index 0000000000..b93ca59e79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/test_request.md @@ -0,0 +1,56 @@ +--- +name: Manual testing checklist +about: Checklist of testing process +title: '' +labels: '' +assignees: '' + +--- + +**Initial set-up** + +*Check that the IntelliJ Idea UTBot plugin can be successfully installed* + +- [ ] Choose appropriate workflow from the next list (by default, use the latest one) https://github.com/UnitTestBot/UTBotJava/actions/workflows/publish-plugin-and-cli.yml +- [ ] Open IntelliJ IDE +- [ ] Remove previously installed UTBot plugin +- [ ] Clone or reuse UTBot project (https://github.com/UnitTestBot/UTBotJava.git) +- [ ] Open the project in the IDE +- [ ] Install the downloaded plugin + +*Go through manual scenarios* + +**Manual scenario #1** + +- [ ] Use default plugin settings +- [ ] Open the utbot-sample/src/main/java/org/utbot/examples/algorithms/ArraysQuickSort.java file +- [ ] Generate tests for the class +- [ ] Remove results +- [ ] Generate tests for the methods + + +**Manual scenario #2** + +- [ ] Use default plugin settings +- [ ] Open the utbot-sample/src/main/java/org/utbot/examples/mock/CommonMocksExample.java file +- [ ] Generate tests with all available (mocking) options + + +**Manual scenario #3** + +- [ ] Create a new Gradle project +- [ ] Add a simple java file to test +- [ ] Generate a test with a new test root + + +**Manual scenario #4** + +- [ ] Create a new Maven project +- [ ] Add a simple java file to test +- [ ] Generate a test with a new test root + +**Manual scenario #5** + +- [ ] Create a new Idea project +- [ ] Add a simple java file to test +- [ ] Generate tests for several classes diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/pull_request_template.md similarity index 66% rename from .github/PULL_REQUEST_TEMPLATE/pull_request_template.md rename to .github/pull_request_template.md index 611921f91c..a9313c6e01 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,9 +8,10 @@ Fixes # (issue) Please delete options that are not relevant. -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- Minor bug fix (non-breaking small changes) +- Bug fix (non-breaking change which fixes an issue) +- New feature (non-breaking change which adds functionality) +- Breaking change (fix or feature that would cause existing functionality to not work as expected) # How Has This Been Tested? @@ -18,14 +19,13 @@ Please delete options that are not relevant. Specify tests that help to verify the change automatically. -- [ ] Test A -- [ ] Test B +_Example:_ org.utbot.examples.algorithms.BinarySearchTest ## Manual Scenario Please, provide several scenarios that you went through to verify that the change worked as expected. -# Checklist: +# Checklist (remove irrelevant options): - [ ] The change followed the style guidelines of the UTBot project - [ ] Self-review of the code is passed @@ -33,4 +33,4 @@ Please, provide several scenarios that you went through to verify that the chang - [ ] New documentation is provided or existed one is altered - [ ] No new warnings - [ ] Tests that prove my change is effective -- [ ] All tests pass locally with my changes \ No newline at end of file +- [ ] All tests pass locally with my changes diff --git a/.github/workflows/build-and-run-tests-from-branch.yml b/.github/workflows/build-and-run-tests-from-branch.yml index 0e581ee963..e6e8798bac 100644 --- a/.github/workflows/build-and-run-tests-from-branch.yml +++ b/.github/workflows/build-and-run-tests-from-branch.yml @@ -1,4 +1,4 @@ -name: "UTBot Java: build and run tests" +name: "[M] UTBot Java: build and run tests" on: workflow_dispatch @@ -13,6 +13,7 @@ jobs: java-version: '8' distribution: 'zulu' java-package: jdk+fx + cache: gradle - uses: gradle/gradle-build-action@v2 with: gradle-version: 6.8 diff --git a/.github/workflows/issue-to-project.yml b/.github/workflows/issue-to-project.yml new file mode 100644 index 0000000000..07f44137e2 --- /dev/null +++ b/.github/workflows/issue-to-project.yml @@ -0,0 +1,22 @@ +name: Add issues to UTBot Java project + +on: + issues: + types: + - opened + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/UnitTestBot/projects/2 + github-token: ${{ secrets.COPY_ISSUE_TO_PROJECT }} + + - uses: actions/add-to-project@main + with: + project-url: https://github.com/orgs/UnitTestBot/projects/5 + github-token: ${{ secrets.COPY_ISSUE_TO_PROJECT }} + diff --git a/.github/workflows/publish-cli-image.yml b/.github/workflows/publish-cli-image.yml new file mode 100644 index 0000000000..fbc98c35a9 --- /dev/null +++ b/.github/workflows/publish-cli-image.yml @@ -0,0 +1,73 @@ +name: "CLI: publish image" +on: + push: + branches: [main] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: utbot_java_cli + DOCKERFILE_PATH: docker/Dockerfile_java_cli + +jobs: + build-and-publish-docker: + runs-on: ubuntu-20.04 + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Set timezone + uses: szenius/set-timezone@v1.0 + with: + timezoneLinux: "Europe/Moscow" + + - name: Set environment variables + run: + echo "COMMIT_SHORT_SHA="$(git rev-parse --short HEAD)"" >> $GITHUB_ENV + + - name: Set docker tag + run: + echo "DOCKER_TAG="$(date +%Y).$(date +%-m).$(date +%-d)-${{ env.COMMIT_SHORT_SHA }}"" >> $GITHUB_ENV + + - name: Log in to the Container registry + uses: docker/login-action@v1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ env.DOCKER_TAG }} + + - name: Docker Buildx (build and push) + run: | + docker buildx build \ + -f ${{ env.DOCKERFILE_PATH }} \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache-new" \ + --tag ${{ steps.meta.outputs.tags }} \ + --build-arg ACCESS_TOKEN=${{ secrets.GITHUB_TOKEN }} \ + --push . + + # Temp fix + # https://github.com/docker/build-push-action/issues/252 + # https://github.com/moby/buildkit/issues/1896 + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/.github/workflows/publish-on-github-packages.yml b/.github/workflows/publish-on-github-packages.yml new file mode 100644 index 0000000000..9fb1e3e2cf --- /dev/null +++ b/.github/workflows/publish-on-github-packages.yml @@ -0,0 +1,61 @@ +name: "[M] Publish on GitHub Packages" + +on: + workflow_dispatch: + inputs: + commit-sha: + type: string + required: true + description: "commit SHA: e.g. cab4799c" + +jobs: + publish_on_github_packages: + if: ${{ github.actor == 'korifey' || github.actor == 'denis-fokin' || github.actor == 'victoriafomina' || + github.actor == 'bissquit' }} + runs-on: ubuntu-20.04 + permissions: + packages: write + contents: read + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'zulu' + java-package: jdk+fx + cache: gradle + - uses: gradle/gradle-build-action@v2 + with: + gradle-version: 6.8 + + - name: Check out ${{ github.event.inputs.commit-sha }} commit + run: | + git fetch + git checkout ${{ github.event.inputs.commit-sha }} + + - name: "UTBot Java: build and run tests" + run: | + export KOTLIN_HOME="/usr" + gradle clean build --no-daemon + + - name: Upload utbot-framework logs + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: utbot_framework_logs + path: utbot-framework/logs/* + + - name: Upload utbot-framework tests report artifacts if tests have failed + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: utbot_framework_tests_report + path: utbot-framework/build/reports/tests/test/* + + - uses: gradle/gradle-build-action@v2 + with: + gradle-version: 6.8 + arguments: publish + env: + GITHUB_ACTOR: ${{ github.actor }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish-plugin-and-cli-from-branch.yml b/.github/workflows/publish-plugin-and-cli-from-branch.yml index 847442adf0..af6006cb32 100644 --- a/.github/workflows/publish-plugin-and-cli-from-branch.yml +++ b/.github/workflows/publish-plugin-and-cli-from-branch.yml @@ -1,4 +1,4 @@ -name: "Plugin and CLI: publish as archives" +name: "[M] Plugin and CLI: publish as archives" on: workflow_dispatch: @@ -24,6 +24,7 @@ jobs: java-version: '8' distribution: 'zulu' java-package: jdk+fx + cache: gradle - uses: gradle/gradle-build-action@v2 with: gradle-version: 6.8 @@ -44,6 +45,7 @@ jobs: gradle buildPlugin --no-daemon -PsemVer=${{ env.VERSION }} cd utbot-intellij/build/distributions unzip utbot-intellij-${{ env.VERSION }}.zip + rm utbot-intellij-${{ env.VERSION }}.zip - name: Archive UTBot IntelliJ IDEA plugin uses: actions/upload-artifact@v2 diff --git a/.github/workflows/publish-plugin-and-cli.yml b/.github/workflows/publish-plugin-and-cli.yml index df2ac0fc41..5629063d3f 100644 --- a/.github/workflows/publish-plugin-and-cli.yml +++ b/.github/workflows/publish-plugin-and-cli.yml @@ -28,6 +28,7 @@ jobs: gradle buildPlugin --no-daemon -PsemVer=${{ env.VERSION }} cd utbot-intellij/build/distributions unzip utbot-intellij-${{ env.VERSION }}.zip + rm utbot-intellij-${{ env.VERSION }}.zip - name: Archive UTBot IntelliJ IDEA plugin uses: actions/upload-artifact@v2 diff --git a/.github/workflows/run-chosen-tests-from-branch.yml b/.github/workflows/run-chosen-tests-from-branch.yml new file mode 100644 index 0000000000..968a2982fe --- /dev/null +++ b/.github/workflows/run-chosen-tests-from-branch.yml @@ -0,0 +1,60 @@ + +name: "[M] Run chosen tests" + +on: + workflow_dispatch: + inputs: + project-name: + type: choice + description: "Project you want to run tests for." + required: true + default: utbot-framework + options: + - utbot-analytics + - utbot-cli + - utbot-framework-api + - utbot-framework + - utbot-fuzzers + - utbot-gradle + - utbot-instrumentation-tests + - utbot-instrumentation + - utbot-intellij + tests-bunch-name: + type: string + required: true + description: "{package-name}.{class-name}.{test-name-optional}" + +jobs: + run-chosen-tests: + runs-on: ubuntu-20.04 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'zulu' + java-package: jdk+fx + cache: gradle + - uses: gradle/gradle-build-action@v2 + with: + gradle-version: 6.8 + + - name: Run chosen tests + run: | + export KOTLIN_HOME="/usr" + gradle :${{ github.event.inputs.project-name }}:test --tests ${{ github.event.inputs.tests-bunch-name }} + + - name: Upload ${{ github.event.inputs.project-name }} tests report if tests have failed + if: ${{ failure() }} + uses: actions/upload-artifact@v2 + with: + name: ${{ github.event.inputs.project-name }}-tests-report + path: ${{ github.event.inputs.project-name }}/build/reports/tests/test/* + + - name: Upload utbot-framework logs if utbot-framework tests have failed + if: ${{ failure() || (github.event.inputs.project-name == 'utbot-framework') }} + uses: actions/upload-artifact@v2 + with: + name: utbot_framework_logs + path: utbot-framework/logs/* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index da0791cf15..2a95592c8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,35 @@ # How to contribute to UTBot Java -## Want a new feature or change existing one? -* Create an issue with proposal and describe your idea -* Wait for our feedback, it will be decided who is preparing the pull request: we may need your help or fix on our side +To begin with, we are very thankful for your time and willingness to read this and contribute! -## Found a defect? -Ensure this defect wasn't already reported in our [Issues](https://github.com/UnitTestBot/UTBotCpp/issues) +The following guideline should help you with suggesting changes to our project, so feel free to use it for your contribution. πŸ˜ƒ -If not - create a new one containing: - * Environment *(describe your full setup)* - * Pre-Conditions *(some special prerequisites)* - * Steps to reproduce *(to be on the same page)* - * Actual and expected result *(what went wrong?)* -If you already have a PR with solution - link it with the created issue. +## I\`ve found a bug! How to report? + +First of all, please check our [Issues](https://github.com/UnitTestBot/UTBotJava/issues) β€” this bug may have already been reported, and you just don\`t need to spend your time on a new one. + +If you haven\`t found the relevant issue, don\`t hesitate to [create a new one](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=bug_report.md&title=), including as much detail as possible β€” the pre-made template will assist you in it. + +In case you already have a PR with a solution, please remain so amazing and link it with the created issue. + + +## I have an improvement suggestion! +Want a new feature or to change the existing one? We are very welcome your fresh ideas. πŸ˜ƒ + +Please [create an issue](https://github.com/UnitTestBot/UTBotJava/issues/new?assignees=&labels=&template=feature_request.md&title=) with your proposal and describe your idea with full information about it. By adding some examples you also bring much happiness to our souls! + +Give us some time to review your proposal and provide you with our feedback. It will be decided who is preparing the pull request: we may need your help or we will take care of it all. πŸ™‚ + + +## Coding conventions +Our team adheres to the defined requirements to coding style to optimize for readability. You can take a look on this [Coding style guide](https://github.com/saveourtool/diktat/blob/master/info/guide/diktat-coding-convention.md) to better understand what we expect to see in your code. + ## How to setup development environment? -Please refer [Developer guide](DEVNOTE.md) to setup developer environment, build and run UTBot. +Please refer [Developer guide](https://github.com/UnitTestBot/UTBotJava/blob/main/DEVNOTE.md) to setup developer environment, build and run UTBot. + ## How to test you PR? diff --git a/DEVNOTE.md b/DEVNOTE.md new file mode 100644 index 0000000000..3fe8ffb9da --- /dev/null +++ b/DEVNOTE.md @@ -0,0 +1,49 @@ +# UTBot Java Developer Guide + + Here are the steps for you to jump into UTBot Java. + +## Install UTBot Java from source +1. Clone UTBot Java repository via [Git](https://github.com/UnitTestBot/UTBotJava.git) +2. Open project in IDE + +![image](https://user-images.githubusercontent.com/106974353/174806216-9d4969b4-51fb-4531-a6d0-94e3734a437a.png) + +⚠️ Important don\`t forgets at this step: + +βœ”οΈ check your Project SDK and Gradle SDK are of 1.8 Java version. + +![image](https://user-images.githubusercontent.com/106974353/174812758-fcbabb5b-0411-48d7-aefe-6d69873185e3.png) +![image](https://user-images.githubusercontent.com/106974353/174806632-ed796fb7-57dd-44b5-b499-e9eeb0436f15.png) + +βœ”οΈ check your System environment variables: the KOTLIN_HOME variable path should be set up + +![image](https://user-images.githubusercontent.com/106974353/175059333-4f3b0083-7964-4886-8fcd-48c475fc1fb3.png) + + +3. Open Gradle tool window +4. Launch Task utbot > Tasks > build > assemble + +![image](https://user-images.githubusercontent.com/106974353/174807962-18c648fd-b67d-4556-90df-eee690abe6e2.png) + +5. Wait for the build to be completed + +Done! You\`re awesome and ready for digging the code. πŸ˜ƒ + + +## Development of UTBot Java with IntelliJ IDEA + +The majority of the code is written in Kotlin. + +The project is divided into Gradle subprojects. The most significant of them are: +1. utbot-framework β€” all about the engine and tests for it + +2. utbot-intellij β€” IDE plugin + +3. utbot-sample β€” a framework with examples to demonstrate engine capacity + +## Testing + +The project contains many tests. They are usually placed in test root of the particular Gradle subproject. + +While developing, it\`s useful to run tests from utbot-framework subproject. The majority of tests from this subproject generate tests for samples from the utbot-sample subproject. + diff --git a/EnableLogging.md b/EnableLogging.md new file mode 100644 index 0000000000..2247b67eb2 --- /dev/null +++ b/EnableLogging.md @@ -0,0 +1,38 @@ +# How to enable UTBot logging in IntelliJ IDEA + +In UTBot we use loggers like the following one: + + +`val logger = Logger.getInstance(CodeGenerator::class.java)` + + +Instead of sending loggers output in the standard stream or a file, we can put them right into IntelliJ IDEA. So, let\`s do it then! πŸ˜ƒ + + +1. In UTBot repository find Gradle > Tasks > intellij > runIde and choose it with the right button click + +![image](https://user-images.githubusercontent.com/106974353/175880783-57a190f1-283d-448f-984b-8acd62af657c.png) + + +2. Select Modify Run Configuration... item, and then Modify options > Specify logs to be shown in console + +![image](https://user-images.githubusercontent.com/106974353/175881032-944bc31a-bd13-43c1-9ebf-e2b542984b7d.png) + + +3. Click βž• and add new Idea log file (or choose any name you want for it πŸ˜‰) in the Log files to be shown in console section + +![image](https://user-images.githubusercontent.com/106974353/175881081-4612493b-a8fb-4c5b-b3b2-edaa4bea0703.png) + + +4. Restart the 'runIde' task, check that the new tab is present in your IDE + +![image](https://user-images.githubusercontent.com/106974353/175881135-6fa393fb-4f62-4f39-b009-dea9bc742411.png) + + +5. And we\`re done! πŸ˜ƒ Narrow logging messages to the loggers you are interested in + +![image](https://user-images.githubusercontent.com/106974353/175881203-9e6e1ed2-3ba7-4ea9-a18a-a5ce314a13ab.png) + + +If you want to use the existing loggers in UTBot or you`re a contributor and you have your own loggers to add, please watch [this article](HowToUseLoggers.md) to know how to do it in a better way. + diff --git a/HowToUseLoggers.md b/HowToUseLoggers.md new file mode 100644 index 0000000000..0988afdf02 --- /dev/null +++ b/HowToUseLoggers.md @@ -0,0 +1,83 @@ +# How to use loggers in UTBotJava + +If you develop different modules of our project like CE, CLI or else, you might need to use loggers or even add your own ones to UTBot. +
Let\`s see how you can work with it. πŸ™‚ + +🟒**1. Find out where appropriate log4j2.xml configuration file is** + +It depends on two factors: + +- Find the project you will run. For instance, for Idea plugin it is **_utbot-intellij_** subproject +- Chose appropriate log4j2.xml. If you are going to run tests for framework, it\`s in the test folder of utbot-framework subproject. + +The file is usually in the resource folder. +
+
+
+🟒**2. Find out the logger name** + +The easiest way is: + +- Go in the code that you are going to debug. Let’s assume it is a method in org.utbot.framework.plugin.api.UtBotTestCaseGenerator. +- Find out if there is a KotlinLogging object that is used to create a **logger** +- If such a logger exists, use the fully qualified class name as the logger name in the next steps +
+ +🟒**3. Add logger** + +Open log4j2.xml and add the logger in the loggers section like this + +``` + + + +``` +
+
+ +🟒**4. Logger level** + +Depending on the desired amount of information, change the **level** attribute. If you do not see output in the console, the wrong level value could be the reason. The **trace** is most verbose level. +
+
+ +🟒**5. Output** + +Sometimes the logging information could be printed in a wrong destination. In that case, change the AppenderRef tag. It can be used with Console value or some other value (for instance, FrameworkAppender) +
+
+ +🟒**6. Message format** + +If you do not like the format for logging output, you can change it in PatternLayout tag (see log4j2.xml in utbot-framework/src/test/resources/) +
+
+ +🟒**7. Multiple loggers** + +Sometimes it is handy to add an extra logger to a Kotlin class in order to log different functionality independently. + +The primary logger is usually defined as + +`private val logger = KotlinLogging.logger {} ` + + +You may add an extra logger + +`private val timeoutLogger = KotlinLogging.logger(logger.name + ".timeout") ` + + +Having this logger, you can use it in code with different log levels in parallel with the primary logger. +
+
+ +🟒**7.1 To enable the logger** + +1. Go to the file you are currently working on +2. Select the file in the project tab (alt-f1) +3. Find the closest log4j2.xml file (usually it is located in the resources file), enable the logger with a desirable log level + + +`` + + diff --git a/README.md b/README.md index 3da38dc837..53ff4ab039 100644 --- a/README.md +++ b/README.md @@ -5,17 +5,23 @@ UTBotJava generates test cases by code, trying to cover maximum statements and execution paths. We treat source code as source of truth assuming that behavior is correct and corresponds to initial user demand. Generated tests are placed in so-called regression suite. Thus, we fixate current behavior by generated test cases. Using UTBotJava developers obtain full control of their code. No future change can break the code without being noticed once it's covered with tests generated by UTBot. This way, modifications made by developers to an existing code are much safer. Hence, with the help of generated unit tests, UTBot provides dramatic code quality improvement. -# How to install UTBot Java IntelliJ IDEA plugin +# UTBot Java IntelliJ IDEA plugin -For now, you can use UTBot under: -- Ubuntu 20.04 -- Windows 10 +UTBot Java provides users with **IntelliJ IDEA** plugin. -See [step-by-step guide](https://github.com/UnitTestBot/UTBotJava/wiki/intellij-idea-plugin) explaining how to install UTBot Java IntelliJ IDEA plugin. +_The plugin was tested on **Win64**, **Linux64** and **MacOSx86_64**._ -# How to use UTBot Java IntelliJ IDEA plugin +# How to download IntelliJ IDEA plugin -See [step-by-step guide](https://github.com/UnitTestBot/UTBotJava/wiki/Generate-tests-with-UTBot-IntelliJ-IDEA-plugin) explaining how to use UTBot Java IntelliJ IDEA plugin. +You can download the plugin from [GitHub Releases](https://github.com/UnitTestBot/UTBotJava/releases). + +# How to install IntelliJ IDEA plugin + +See [step-by-step guide](https://github.com/UnitTestBot/UTBotJava/wiki/intellij-idea-plugin) explaining how to install the plugin. + +# How to use IntelliJ IDEA plugin + +See [step-by-step guide](https://github.com/UnitTestBot/UTBotJava/wiki/generate-tests-with-plugin) explaining how to use the plugin. # How to contribute to UTBot Java diff --git a/build.gradle b/build.gradle index f18cfd3051..02ef9119b3 100644 --- a/build.gradle +++ b/build.gradle @@ -27,8 +27,41 @@ subprojects { apply plugin: 'java' apply plugin: 'maven-publish' + publishing { + publications { + jar(MavenPublication) { + from components.java + groupId 'org.utbot' + artifactId project.name + } + } + } + repositories { mavenCentral() maven { url 'https://jitpack.io' } } -} \ No newline at end of file +} + +configure([ + project(':utbot-api'), + project(':utbot-core'), + project(':utbot-framework'), + project(':utbot-framework-api'), + project(':utbot-fuzzers'), + project(':utbot-instrumentation'), + project(':utbot-summary') +]) { + publishing { + repositories { + maven { + name = "GitHubPackages" + url = "https://maven.pkg.github.com/UnitTestBot/UTBotJava" + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } + } +} diff --git a/docker/Dockerfile_java_cli b/docker/Dockerfile_java_cli new file mode 100644 index 0000000000..35c9536c62 --- /dev/null +++ b/docker/Dockerfile_java_cli @@ -0,0 +1,26 @@ +FROM openjdk:8 + +ARG ACCESS_TOKEN + +WORKDIR /usr/src/ + +RUN apt-get update \ + && apt-get install -y curl \ + unzip \ + python3 \ + python3-requests \ + && apt-get clean + +# Install UTBot Java CLI +COPY docker/get_java_cli_download_url.py . + +ENV JAVA_CLI_ZIP_NAME "utbot_java_cli.zip" + +RUN curl -H "Authorization: Bearer ${ACCESS_TOKEN}" \ + -L "$(python3 get_java_cli_download_url.py)" \ + -o "${JAVA_CLI_ZIP_NAME}" \ + && unzip "${JAVA_CLI_ZIP_NAME}" \ + && rm "${JAVA_CLI_ZIP_NAME}" + +ENV JAVA_CLI_PATH="$(find /usr/src -type f -name utbot-cli*)" +RUN ln -s "${JAVA_CLI_PATH}" $pwd/utbot-cli.jar diff --git a/docker/get_java_cli_download_url.py b/docker/get_java_cli_download_url.py new file mode 100644 index 0000000000..ad2f6b03a5 --- /dev/null +++ b/docker/get_java_cli_download_url.py @@ -0,0 +1,14 @@ +import json +import requests + +JAVA_ARTIFACTS_URL="https://api.github.com/repos/UnitTestBot/UTBotJava/actions/artifacts" + +request = requests.get(url = JAVA_ARTIFACTS_URL) +data = request.json() +artifacts = data['artifacts'] + +for artifact in artifacts: + if "utbot-cli" in artifact['name']: + print(artifact['archive_download_url']) + break + diff --git a/docs/AssumptionsMechanism.md b/docs/AssumptionsMechanism.md new file mode 100644 index 0000000000..487c69ddef --- /dev/null +++ b/docs/AssumptionsMechanism.md @@ -0,0 +1,216 @@ +# Assumptions mechanism +We have a public API that might help both us and external users to interact with UTBot. +This document contains detailed instructions about how to use `assume` methods of `UtMock` class and description +of the problems we encountered during the implementation. + +## Brief description +This section contains short explanations of the meaning for mentioned functions and examples of usage. + +### UtMock.assume(predicate) + +`assume` is a method that gives the opportunity for users to say to the symbolic virtual machine that +instructions of the MUT (Method Under Test) encountered after this instruction satisfy the given predicate. +It is natively understandable concept and the closest analog from Java is the `assert` function. +When the virtual machine during the analysis encounters such instruction, it drops all the branches in the +control flow graph violating the predicate. + +Examples: +```java +int foo(int x) { + // Here `x` is unbounded symbolic variable. + // It can be any value from [MIN_VALUE..MAX_VALUE] range. + + UtMock.assume(x > 0); + // Now engine will adjust the range to (0..MAX_VALUE]. + + if (x <= 0) { + throw new RuntimeException("Unreachable exception"); + } else { + return 0; + } +} + +// A function that removes all the branches with a null, empty or unsorted list. +public List sortedList(List a) { + // An invariant that the list is non-null, non-empty and sorted + UtMock.assume(a != null); + UtMock.assume(a.size() > 0); + UtMock.assume(a.get(0) != null); + + for (int i = 0; i < a.size() - 1; i++) { + Integer element = a.get(i + 1) + UtMock.assume(element != null); + UtMock.assume(a.get(i) <= element); + } + + return a; +} +``` + +Thus, `assume` is a mechanism to provide some known properties of the program to the symbolic engine. + +### UtMock.assumeOrExecuteConcretely(predicate) +It is a similar concept to the `assume` with the only difference: it does not drop +paths violating the predicate, but execute them concretely from the moment of the +first encountered controversy. Let's take a look at the example below: + +```java +int foo(List list) { + // Let's assume that we have small lists only + UtMock.assume(list.size() <= 10); + + if (list.size() > 10) { + throw new RuntimeException("Unreachable branch"); + } else { + return 0; + } +} +``` +Here we decided to take into consideration lists with sizes less or equal to 10 to improve performance, and, therefore, lost +the branch with possible exception. Let's change `assume` to `assumeOrExecuteConcretely`. +```java +int foo(List list) { + // Let's assume that we have small lists only + UtMock.assumeOrExecuteConcretely(list.size() <= 10); + + if (list.size() > 10) { + throw new RuntimeException("Now we will cover this branch"); + } else { + return 0; + } +} +``` +Now we will cover both branches of the MUT. How did we do it? +At the moment we processed `if` condition we got conflicting constraints: `list.size <= 10` and +`list.size > 10`. In contrast to the example with `assume` method usage, here we know that +we got conflict with the provided predicate and we stop symbolic analysis, remove this assumption from +the path constraints, resolve the MUT's parameters and run the MUT using concrete execution. + +Thus, `assumeOrExecuteConcretely` is a way to provide to the engine information about the program +you'd like to take into account, but if it is impossible, the engine will run the MUT concretely trying to +cover at least something after the encountered controversy. + +## Implementation +Implementation of the `assume` does not have anything interesting -- +we add the predicate into path hard-constraints, and it eventually removes violating +paths from consideration. + +Processing a predicate passed as argument in `assumeOrExecuteConcretely` is more tricky. +Due to the way we work with the solver, it cannot be added to the path constraints +directly. We treat hard PC as hypothesis and add them to the solver directly, that deprives us +opportunity to calculate unsat-core to check whether the predicate was a part of it. + +Because of it, we introduced an additional type of path constraints. Now we have three of them: +hard, soft and assumptions. +* Hard constraints -- properties of the program that must be satisfied at any point of the program. +* Soft constraints -- properties of the program we want to satisfy, but we can remove them if it is impossible. +For example, it might be information that some number should be less than zero. But if it is not, we still +can continue exploration of the same path without this constraint. +* Assumptions -- predicates passed in the `assumeOrExecuteConcretely`. If we have a controversy between +an assumption and hard constraints, we should execute the MUT concretely without violating assumption. + +Now, when we check if some state is satisfiable, we put hard constraints as hypothesis into the solver +and check their consistency with soft constraints and assumptions. If the solver returns UNSAT status with +non-empty unsat core, we remove all conflicting soft constraints from it and try again. If we have +UNSAT status for the second time and assumptions in it, we remove them from the request and calculates +status once again. If it is SAT, we put this state in the queue for concrete executions, otherwise -- remove the +state from consideration. + +## Problems +The main problem is that we didn't get the result we expected. We have many `assume` usages +in overridden classes source code that limits their size to improve performance. Because of that, the following +code does not work (we don't generate any executions for them). + +```java +void bigList(List list) { + UtMock.assume(list != null && list.size() > MAX_LIST_SIZE); +} + +void bigSet(Set set) { + UtMock.assume(set != null && set.size() > MAX_SET_SIZE); +} + +void bigMap(Map map) { + UtMock.assume(map != null && map.size() > MAX_MAP_SIZE); +} +``` +The problem in a `preconditionCheck` method of the wrappers. It contains something like that: +```java +private void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + ... + assume(elementData.end >= 0 & elementData.end <= MAX_LIST_SIZE); + ... +} +``` +It is a method that imposes restrictions at the overridden classes provided as arguments. +For `Lists` the idea works fine, we replace `assume` with `assumeOrExecuteConcretely` and +find additional branches. +```java +private void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + ... + assume(elementData.end >= 0); + assumeOrExecuteConcretely(elementData.end <= MAX_LIST_SIZE); + ... +} + +// Now it works! +void bigList(List list) { + UtMock.assume(list != null && list.size() > MAX_LIST_SIZE); +} +``` + +But it doesn't work for `String`, `HashSet` and `HashMap`. + +The problem with `String` is connected with the way we represent them. Restriction for the size is closely +connected with the internal storage of chars -- we create a new arrays of chars with some size `n` using `new char[n]` instruction. +It adds hard constraint that max size of the string is `n` and assumption that `n` is less that `MAX_STRING_SIZE`. +Somewhere later in the code we have a condition that `String.length() > MAX_STRING_SIZE`. Unfortunately, +it will cause not only controversy between the assumption and new hard constraints, but and controversy +between internal array size (hard constraint) and the new-coming hard constraint, that will cause UNSAT status and +we will lose this branch anyway. + +`HashSet` and `HashMap` is a different story. The problem there is inside of `preconditionCheck` implementation. Let's take +a look at a part of it: +```java +assume(elementData.begin == 0); +assume(elementData.end >= 0); +assumeOrExecuteConcretely(elementData.end <= 3); + +parameter(elementData); +parameter(elementData.storage); +doesntThrow(); + +// check that all elements are distinct. +for (int i = elementData.begin; i < elementData.end; i++) { + E element = elementData.get(i); + parameter(element); + // make element address non-positive + + // if key is not null, check its hashCode for exception + if (element != null) { + element.hashCode(); + } + + // check that there are no duplicate values + // we can start with a next value, as all previous values are already processed + for (int j = i + 1; j < elementData.end; j++) { + // we use Objects.equals to process null case too + assume(!Objects.equals(element, elementData.get(j))); + } +} + +visit(this); +``` +The problem happens at the first line of the cycle. We now (from the first line of the snippet) that +the cycle will be from zero to three. The problem is in the `i < elementData.end` check. It produces +at the fourth iteration hard constraint that `elementData.begin + 4 < elementData.end` and we have +an assumption that `elementData.end <= 3`. It will cause a concrete run of the MUT in every +`preconditionCheck` analysis with a constraint `elementData.end == 4`. Moreover, it still won't +help us with code like `if (someHashSet.size() == 10)`, since we will never get here without hard +constraint `elementData.end < 4` that came from the cycle. \ No newline at end of file diff --git a/docs/SpeculativeFieldNonNullability.md b/docs/SpeculativeFieldNonNullability.md new file mode 100644 index 0000000000..b0985acb7c --- /dev/null +++ b/docs/SpeculativeFieldNonNullability.md @@ -0,0 +1,55 @@ +# Speculative field non-nullability assumptions + +## The problem + +When a field is used as a base for a dot call (i.e., a method call or a field access), +the symbolic engine creates a branch corresponding to the potential `NullPointerException` +that can occur if the value of the field is `null`. For this path, the engine generates +the hard constraint `addr(field) == null`. + +If the field is marked as `@NotNull`, a hard constraint `addr(field) != null` is generated +for it. If both constraints have been generated simultaneously, the `NPE` branch is discarded +as the constraint set is unsatisfiable. + +If a field does not have `@NotNull` annotation, the `NPE` branch will be kept. This behavior +is desirable, as it increases the coverage, but it has a downside. It is possible that +most of generated branches would be `NPE` branches, while useful paths could be lost due to timeout. + +Beyond that, in many cases the `null` value of a field can't be generated using the public API +of the class. This is particularly true for final fields, especially in system classes. +Automatically generated tests assign `null` values to fields in questions using reflection, +but these tests may be uninformative as the corresponding `NPE` branches would never occur +in the real code that limits itself to the public API. + +## The solution + +To discard irrelevant `NPE` branches, we can speculatively mark fields we as non-nullable even they +do not have an explicit `@NotNull` annotation. In particular, we can use this approach to final +fields of system classes, as they are usually correctly initialized and are not equal `null`. + +At the same time, we can't always add the "not null" hard constraint for the field: it would break +some special cases like `Optional` class, which uses the `null` value of its final field +as a marker of an empty value. + +The engine checks for NPE and creates an NPE branch every time the address is used +as a base of a dot call (i.e., a method call or a field access); +see `UtBotSymbolicEngine.nullPointerExceptionCheck`). The problem is what at that moment, we would have +no way to check whether the address corresponds to a final field, as the corresponding node +of the global graph would refer to a local variable. The only place where we have the complete +information about the field is this method. + +We use the following approach. If the field is final and belongs to a system class, +we mark it as a speculatively non-nullable in the memory +(see `org.utbot.engine.Memory.speculativelyNotNullAddresses`). During the NPE check +we will add the `!isSpeculativelyNotNull(addr(field))` constraint +to the `NPE` branch together with the usual `addr(field) == null` constraint. + +For final fields, these two conditions can't be satisfied at the same time, as we speculatively +mark final fields as non-nullable. As a result, the NPE branch would be discarded. If a field +is not final, the condition is satisfiable, so the NPE branch would stay alive. + +We limit this approach to the system classes only, because it is hard to speculatively assume +something about non-nullability of final fields in the user code. + +The same approach can be extended for other cases where we want to speculatively consider some +fields as non-nullable to prevent `NPE` branch generation. diff --git a/docs/jlearch/execution-state-changes.md b/docs/jlearch/execution-state-changes.md new file mode 100644 index 0000000000..b3a88d3984 --- /dev/null +++ b/docs/jlearch/execution-state-changes.md @@ -0,0 +1,41 @@ +# Changes in Execution State + +```mermaid +classDiagram + class StateAnalyticsProperties{ + +int depth + +int visitedAfterLastFork + +int visitedBeforeLastFork + +int stmtsSinceLastCovered + +ExecutionState? parent + +long executingTime + +double reward + +List~Double~ features + -boolean isFork + -boolean isVisitedNew + -int successorDepth + -int successorVisitedAfterLastFork + -int successorVisitedBeforeLastFork + -int successorStmtSinceLastCovered + + +updateIsVisitedNew() + +updateIsFork() + } + ExecutionState o-- StateAnalyticsProperties +``` + +`StateAnalyticsProperties` maintains properties of `ExecutionState`, which don't need for symbolic execution, but need for `JLearch`. + +* `depth: Int` - number of forks on the state's path excluded current state, if it is fork. In this case, fork is a state with more than one successor excluded implicit `NPE` branches. +* `visitedAfterLastFork: Int` - number of `stmt`, that was visited by `states` on this state's path after the last fork in first time. +* `visitedBeforeLastFork: Int` - number of `stmt`, that was visited by `states` on this state's path before the last fork in first time. +* `stmtsSinceLastCovered: Int` - number of `states` on this state's path after the last state that visited any `stmt` in first time. +* `parent: ExecutionState?` - parent of current `state`. If `UtSettings.featureProcess == false`, then it is always null, because we don't need this field in this case. If it is not null, then we can't delete `state` until all successors of this state will be deleted, which may cause memory issue. +* `executingTime: Long` - amount of time, during which this state was traversed. +* `reward: Double?` - calculated reward of this state +* `features: List` - list of extracted features for this state + +Field with `successor` prefix is used for a constructor of successor properties. + +* `updateIsFork()` - set `isFork` on true. This method is called when traversing of `stmt` produces more than one explicit state. Now it may be during the traversing of `IfStmt`, `SwitchStmt`, `AssignStmt` or `InvokeStmt`. +* `updateIsCoveredNew()` - set `isVisitedNew` on true, set `stmtsSinceLastCovered` on zero and increase `visitedAfterLastFork` on 1. This method is called in `UtBotSymbolicEngine` after new state `s` is polled and `s.stmt` was not visited yet. diff --git a/docs/jlearch/features.md b/docs/jlearch/features.md new file mode 100644 index 0000000000..1b3ca05a00 --- /dev/null +++ b/docs/jlearch/features.md @@ -0,0 +1,17 @@ +# Collecting features + +Now we collect 13 features, that will be described in original [paper](https://files.sri.inf.ethz.ch/website/papers/ccs21-learch.pdf), except constraint representation, but it can be extended. + +* `stack` - size of state’s current call stack. +* `successor` - number of successors of state’s current basic block. +* `testCase` - number of test cases generated so far +* `coverageByBranch` - number of instructions, which was covered first time on our last branch +* `coverageByPath` - number of instructions, which was covered first time on our path +* `depth` - number of forks already performed along state’s path. +* `cpicnt` - number of instructions visited in state's current function. +* `icnt` - number of times for which st ate’s current instruction has + been visited +* `covNew` - number of instructions executed by st ate since the last + time a new instruction is covered +* `subpath` - number of times for which st ate’s subpaths have been + visited. The length of the subpaths can be 1, 2, 4, or 8 respectively diff --git a/docs/jlearch/jlearch-architecture.md b/docs/jlearch/jlearch-architecture.md new file mode 100644 index 0000000000..0f7c8d1293 --- /dev/null +++ b/docs/jlearch/jlearch-architecture.md @@ -0,0 +1,154 @@ +# JLearch architecture + +# Global Class Diagram + +```mermaid +classDiagram + class FeatureProcessor{ + dumpFeatures() + } + <> FeatureProcessor + class TraverseGraphStatistics{ + onVisit(ExecutionState) + onTraversed(ExecutionState) + } + class InterproceduralUnitGraph + class FeatureExtractorFactory + <> FeatureExtractorFactory + class FeatureProcessorFactory + <> FeatureProcessorFactory + class EngineAnalyticsContext + class UtBotSymbolicEngine + class NNRewardGuidedSelectorFactory + <> NNRewardGuidedSelectorFactory + class FeatureExtractor{ + extractFeatures(ExecutionState) + } + <> FeatureExtractor + + UtBotSymbolicEngine ..> EngineAnalyticsContext + EngineAnalyticsContext o-- FeatureProcessorFactory + EngineAnalyticsContext o-- FeatureExtractorFactory + EngineAnalyticsContext o-- NNRewardGuidedSelectorFactory + + FeatureProcessor --|> TraverseGraphStatistics + InterproceduralUnitGraph o-- TraverseGraphStatistics + UtBotSymbolicEngine *-- FeatureProcessor + UtBotSymbolicEngine *-- InterproceduralUnitGraph + + class Predictors + class StateRewardPredictor + class NNRewardGuidedSelector + + + class GreedySearch + + class BasePathSelector + + GreedySearch --|> BasePathSelector + NNRewardGuidedSelector --|> GreedySearch + + UtBotSymbolicEngine *-- BasePathSelector + + Predictors o-- StateRewardPredictor + NNRewardGuidedSelector ..> Predictors + NNRewardGuidedSelector *-- FeatureExtractor + + NNStateRewardPredictorSmile --|> StateRewardPredictor + StateRewardPredictorTorch --|> StateRewardPredictor + LinearStateRewardPredictor --|> StateRewardPredictor + + NNStateRewardGuidedSelectorWithRecalculationWeight --|> NNRewardGuidedSelector + NNStateRewardGuidedSelectorWithoutRecalculationWeight --|> NNRewardGuidedSelector +``` + +This diagram doesn't illustrate some details, so read them below. + +# FeatureProcessor + +It is interface in framework-module, that allows to use implementation from analytics module. + +* `dumpFeatures(state: ExecutionState)` - dump features and rewards in some format on disk. Called at the end of traverse in `UtBotSymbolicEngine` + +## Implementation class diagram + +```mermaid +classDiagram + class FeatureProcessorWithStatesRepetition{ + -Map~Int, FeatureList~ dumpedStates + -Set~Stmt~ visitedStmts + -List~TestCase~ testCases + -int generatedTestCases + dumpFeatures() + } + + class FeatureExtractor{ + extractFeatures(ExecutionState) + } + + class TraverseGraphStatistics{ + onVisit(ExecutionState) + onTraversed(ExecutionState) + } + + class RewardEstimator{ + calculateRewards(List~TestCase~) + } + + class TestCase{ + +List states + +int newCoverage + +int testIndex + } + + FeatureProcessorWithStatesRepetition --|> TraverseGraphStatistics + FeatureProcessorWithStatesRepetition o-- FeatureExtractor + FeatureProcessorWithStatesRepetition o-- RewardEstimator + FeatureProcessorWithStatesRepetition ..> EngineAnalyticsContext + +``` + +`State = Pair` + +`FeatureList = List): Map` - calculates `coverage` for each state and `time` for each state. `Coverage` - sum of `newCoverage` by `TestCase` that contains its state. `Time` - sum of `state.executingTime` by all states, that has this state on its path. Then calculates `reward(coverage, time)`. + +## FeatureProcessorWithStatesRepetition + +* `onVisit(state: ExecutionState)` - extractFeatures for state +* `onTraversed(state: ExecutionState)` - create `TestCase`, so we go from `state` to `state.parent` while it is not root, for each `state` on path add its features to `dumpedStates`, calculate coverage of its `TestCase`, increment `generatedTestCases` on 1 and add new `TestCase` in `testCases`. +* `dumpFeatures()` - call `RewardEstimator.calculateRewards()` and write `csv` file for each `TestCase` in format: `newCov,features,reward` for each `state` in it. `newCov` - flag that indicates whether this `TestCase` cover something new or not. So in this approach, each `state` will be written as many times as the number of `TestCase` that has it. +For creating `FeatureExtractor`, it uses `FeatureExtractorFactory` from `EngineAnalyticsContext`. + +# FeatureExtractor + +It is interface in framework-module, that allows to use implementation from analytics module. +* `extractFeatures(state: ExecutionState)` - create features list for state and store it in `state.features`. Now we extract all features, which were described in [paper](https://files.sri.inf.ethz.ch/website/papers/ccs21-learch.pdf). In feature, we can extend the feature list by other features, for example, NeuroSMT. + +# StateRewardPredictor + +Interface for reward predictors. Now it has three implementations in `analytics` module: + +* `NNStateRewardPredictorSmile`: it uses our own format to store feedforward neural network, and it uses `Smile` library to do multiplication of matrix. +* `NNStateRewardPredictorTorch`: it assumed that a model is any type of model in `pt` format. It uses the `Deep Java library` to use such models. +* `LinearStateRewardPredictor`: it uses our own format to store weights vector: line of doubles, separated by comma with bias as last weight. + +It should be created at the beginning of work and stored at `Predictors` class to be used in `NNRewardGuidedSelector` from the `framework` module. + + +# NNStateRewardGuidedSelector + +It uses an `EngineAnalyticsContext` to create `FeatureExtractor`. +We override `ExecutionState.weight` as `NNStateRewardPredictor.predict(this.features)`. +We have two different implementantions: +* `NNStateRewardGuidedSelectorWithRecalculation`: we recalculate reward every time, so in `ExecutionState.weight` we extract features and call predict. +* `NNStateRewardGuidedSlectorWithoutRecalculation`: we extract features in `offerImpl`, calculate `reward` and store it in `ExecutionState.reward` without recalculation it every time. + +# EngineAnalyticsContext + +It is an object that should be filled by factories in the beginning of work to allow objects from the `framework` module using objects from `analytics` module. diff --git a/docs/jlearch/new-heuristical-path-selectors.md b/docs/jlearch/new-heuristical-path-selectors.md new file mode 100644 index 0000000000..756abb2c0e --- /dev/null +++ b/docs/jlearch/new-heuristical-path-selectors.md @@ -0,0 +1,93 @@ +# GreedySearch + +```mermaid +classDiagram + class GreedySearch{ + -Set~ExecutionState~ states + +ExecutionState.weight + } + GreedySearch --|> BasePathSelector +``` +Base methods such as `offer` or `remove` is implemented pretty simple and just a delegation to `states`. + +In `peekImpl` we find the set of `states` with maximum `weight` and peek random among them, so to use this class in implementation of some `pathSelector`, you just need to override an `ExecutionState.weight`. + +# SubpathStatistics + +```mermaid +classDiagram + class SubpathStatistics{ + +int index + -Map~Subpath, Int~ subpathCount + subpathCount(ExecutionState) + } + class TraverseGraphStatistics{ + onVisit(ExecutionState) + } + + SubpathStatistics --|> TraverseGraphStatistics + TraverseGraphStatistics o-- InterProceduralUnitGraph +``` +`Subpath` = `List` + +This class maintains frequency of each subpath with length `2^index`, which is presented as `List`, in a certain instance of `InterproceduralUnitGraph` + +* `onVisit(state: ExecutionState)` - we calculate subpath of this state and increment its frequency on `1` +* `subpathCount(state: ExecutionState)` - we calculate subpath of this state and return its frequency + +# SubpathGuidedSelector + +```mermaid +classDiagram + SubpathGuidedSelector o-- SubpathStatistics + SubpathGuidedSelector --|> GreedySearch +``` + +Inspired by [paper](http://pxzhang.cn/paper/concolic_testing/oopsla13-pgse.pdf). + +We override `ExecutionState.weight` as `-StatementStatistics.subpathCount(this)`, so we pick the `state`, which `subpath` is less traveled. + +# StatementStatistics + +```mermaid +classDiagram + class StatementStatistics{ + -Map~Stmt, Int~ statementsCount + -Map~SootMethod, Int~ statementsInMethodCount + +statementCount(ExecutionState) + +statementsInMethodCount(ExecutionState) + } + + class TraverseGraphStatistics{ + onVisit(ExecutionState) + } + + StatementStatistics --|> TraverseGraphStatistics + TraverseGraphStatistics o-- InterProceduralUnitGraph +``` + +This class maintains frequency of each `Stmt` and number of `Stmt`, that was visited in some `SootMethod`, on a certain instance of `InterproceduralUnitGraph`. + +* `onVisit(state: ExecutionState)` - increment frequency of state's `stmt` on 1. If we visit this `stmt` for the first time, then increment number of `Stmt`, that we visit in the current state's `method`, on 1. +* `statementCount(state: ExecutionState)` - get a frequency of state's `stmt` +* `statementsInMethodCount(state: ExecutionState)` - get number of `stmt`, that was visited in the current state's `method`. + +# CPInstSelector + +```mermaid +classDiagram + CPInstSelector o-- StatementStatistics + CPInstSelector --|> NonUniformRandomSearch +``` + +Override `ExecutionState.cost` as `StatementStatistics.statementInMethodCount(this)`, so we are more likely to explore the least explored `method`. + +# ForkDepthSelector + +```mermaid +classDiagram + ForkDepthSelector --|> NonUniformRandomSearch +``` + +Override `ExecutionState.cost` as `ExecutionState.depth`, so we are more likely to explore the least deep `state` in terms of the number of forks on its path. + diff --git a/docs/jlearch/pipeline-training-usage.md b/docs/jlearch/pipeline-training-usage.md new file mode 100644 index 0000000000..32cbd0dddf --- /dev/null +++ b/docs/jlearch/pipeline-training-usage.md @@ -0,0 +1,37 @@ +# Pipeline diagram + +```mermaid +graph TD + Projects --> ContestEstimator + Selectors --> ContestEstimator + subgraph FeatureGeneration + ContestEstimator --> Tests + Tests --> Features + Tests --> Rewards + end + + + Features --> Data + Rewards --> Data + + Data --> Models + Models --> NNRewardGuidedSelector --> UsualTestGeneration +``` + +# Training + +Briefly: + +* Get dataset `D` by running `ContestEstimator` on several projects using several selectors. +* Train `model_0` using `D` +* For several `iterations` repeat (assume we on `i`-th step): + * Get dataset `D'` by running `ContestEstimator` on several projects using `NNRewardGuidedSelector`, which will use `model_i` + * $$D = D \cup D'$$ + * Train `model_$(i+1)` using `D` + +To do this, you should: +* Be sure that you use `Java 8` by `java` command and set `JAVA_HOME` to `Java 8`. +* Put projects, on which you want to learn in `contest_input/projects` folder, then list classes, on which you want to learn in `contest_input/classes//list` (if it is empty, than we will take all classes from project jar). +* Run `pip install -r scripts/requirements.txt`. It is up to you to make it in virtual environment or not. +* List selectors in `scripts/selector_list` and projects in `scripts/prog_list` +* Run `./scripts/train_iteratively.sh ` diff --git a/docs/jlearch/scripts.md b/docs/jlearch/scripts.md new file mode 100644 index 0000000000..64b4294f47 --- /dev/null +++ b/docs/jlearch/scripts.md @@ -0,0 +1,65 @@ +# How to use scripts +For each scenario: go to root of `UTBotJava` repository - it is `WORKDIR`. + +`PATH_SELECTOR` as argument is `"PATH_SELECTOR_TYPE [PATH_SELECTOR_PATH for NN] [IS_COMBINED (false by default)] [ITERATIONS]"`. + +Before start of work run: +```bash +./scripts/prepare.sh +``` + +It will copy contest resources in `contest_input` folder and build the project, because we use jars, so if you want to change something in code and re-run scripts, then you should run: +```bash +./gradlew clean build -x test +``` + +## To Train a few iterations of your models: +By default features directory is `eval/features` - it should be created, to change it you should manually do it in source code of scripts. + +List projects and selectors on what you want to train in `scripts/prog_list` and `scripts/selector_list`. You will be trained on all methods of all classes from `contest_input/classes//list`. + +Then just run: +```bash +./scripts/train_iteratively.sh +``` +Python command is your command for python3, in the end of execution you will get iterations models in `` folder and features for each selector and project in `//` for `selector` from `selectors_list` and in `/jlearch//` for models. + +## To Run Contest Estimator with coverage: +Check that `srcTestDir` with your project exist in `build.gradle` of `utbot-junit-contest`. If it is not then add `build/output/test/`. + +Then just run: +```bash +./scripts/run_with_coverage.sh +``` + +In the end of execution you will get jacoco report in `eval/jacoco///` folder. + +## To estimate quality +Just run: +```bash +./scripts/quality_analysis.sh +``` +It will take coverage reports from relative report folders (at `eval/jacoco/project/alias`) and generate charts in `$outputDir//.html`. +`outputDir` can be changed in `QualityAnalysisConfig`. Result file will contain information about 3 metrics: +* $\frac{\sum_{c \in classSet} instCoverage(c)}{|classSet|}$ +* $\frac{\sum_{c \in classSet} coveredInstructions(c)}{\sum_{c \in classSet} allInstructions(c)}$ +* $\frac{\sum_{c \in classSet} branchCoverage(c)}{|classSet|}$ + +For each metric for each selector you will have: +* value of metric +* some chart with median, $q_1$, $q_3$ and so on + + +## To scrap solution classes from codeforces +Note: You can't scrap many classes, because codeforces api has a request limit. + +It can be useful, if you want to train Jlearch on classes usually without virtual functions, but with many algorithms, so cycles and conditions. + +Just run: +```bash +python3 path/to/codeforces_scrapper.py --problem_count --submission_count --min_rating --max_rating --output_dir +``` + +All arguments are optional. Default values: `100`, `10`, `0`, `1500`, `.`. + +At the end you should get `submission_count` classes for each of `problem_count` problems with rating between `min_rating` and `max_rating` at `output_dir`. Each class have package `p.p`. \ No newline at end of file diff --git a/docs/jlearch/setup.md b/docs/jlearch/setup.md new file mode 100644 index 0000000000..d1bf89c364 --- /dev/null +++ b/docs/jlearch/setup.md @@ -0,0 +1,35 @@ +# How to setup environment for experiments on Linux + +* Clone repository, go to root +* `chmod +x ./scripts/*` and `chmod +x gradlew`. +* Set `Java 8` as default and set `JAVA_HOME` to this `Java`. + For example + * Go through [this](https://sdkman.io/install) until `Windows installation` + * `sdk list java` + * Find any `Java 8` + * `sdk install ` + * `sdk use ` + * Check `java -version` +* `mkdir -p eval/features` +* `mkdir models` +* Set environment for `Python`. + For example + * `python3 -m venv /path/to/new/virtual/environment` + * `source /path/to/venv/bin/activate` + * Check `which python3`, it should be somewhere in `path/to/env` folder. + * `pip install -r scripts/requirements.txt` +* `./scripts/prepare.sh` +* Change `scripts/prog_list` to run on smaller project or delete some classes from `contest_input/classes//list`. + +# Default settings and how to change it +* You can reduce number of models in `models` variable in `scripts/train_iteratively.sh` +* You can change amount of required RAM in `run_contest_estimator.sh`: `16 gb` by default +* You can change `batch_size` or `device` n `train.py`: `4096` and `gpu` by default +* If you are completing setup on server, then you will need to uncomment tmp directory option in `run_contest_estimator.sh` + +# Continue setup +* `scripts/train_iteratively.sh 30 2 models ` +* In `models/` you should get models. +* `mkdir eval/jacoco` +* `./scripts/run_with_coverage.sh 30 "NN_REWARD_GUIDED_SELECTOR path/to/model" some_alias`. `path/to/model` should be something like `models/nn32/0`, where `nn32` is a type of model and `0` is the iteration number +* You should get jacoco report in `eval/jacoco/guava-26.0/some_alias/` \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2dd82c1232..cc0c15f08d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -30,4 +30,18 @@ testng_version=7.4.0 mockito_inline_version=4.0.0 jackson_version = 2.12.3 javasmt_solver_z3_version=4.8.9-sosy1 -# soot also depends on asm, so there could be two different versions \ No newline at end of file +slf4j_version=1.7.36 +eclipse_aether_version=1.1.0 +maven_wagon_version=3.5.1 +maven_plugin_api_version=3.8.5 +maven_plugin_tools_version=3.6.4 +maven_plugin_testing_version=3.3.0 +maven_resolver_api_version=1.8.0 +sisu_plexus_version=0.3.5 +javacpp_version=1.5.3 +jsoup_version=1.7.2 +djl_api_version=0.17.0 +pytorch_native_version=1.9.1 +# soot also depends on asm, so there could be two different versions + +kotlin.stdlib.default.dependency=false \ No newline at end of file diff --git a/scripts/codeforces_scrapper/codeforces_scrapper.py b/scripts/codeforces_scrapper/codeforces_scrapper.py new file mode 100644 index 0000000000..d6ae207c1c --- /dev/null +++ b/scripts/codeforces_scrapper/codeforces_scrapper.py @@ -0,0 +1,120 @@ +import argparse +import os.path +import time + +import requests +from urllib import request +import json +import bs4 +import javalang + +from codeforces import CodeforcesAPI + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--problem_count", dest='problem_count', type=int, default=100) + parser.add_argument("--submission_count", dest='submission_count', type=int, default=10) + parser.add_argument("--min_rating", dest='min_rating', type=int, default=0) + parser.add_argument("--max_rating", dest="max_rating", type=int, default=1500) + parser.add_argument("--output_dir", dest="output_dir", type=str, default=".") + return parser.parse_args() + + +args = get_args() + + +def check_json(answer): + values = json.loads(answer) + + if values['status'] == 'OK': + return values['result'] + + +def get_main_name(tree): + """ + :return: class name with main method + """ + return next(klass.name for klass in tree.types + if isinstance(klass, javalang.tree.ClassDeclaration) + for m in klass.methods + if m.name == 'main' and m.modifiers.issuperset({'public', 'static'})) + + +def save_source_code(contest_id, submission_id): + """ + Parse html page to find source code of submission and save it in some unique package p${contest_id}.p${submission_id}. + If we reach api request bound, then we try to sleep for 5 minutes. + """ + url = request.Request(f"http://codeforces.com/contest/{contest_id}/submission/{submission_id}") + with request.urlopen(url) as req: + soup = bs4.BeautifulSoup(req.read(), "html.parser") + path = os.path.join(args.output_dir, f"p{contest_id}", f"p{submission_id}") + if not os.path.exists(path): + os.makedirs(path) + code = "" + for p in soup.find_all("pre", {"class": "program-source"}): + code += p.get_text() + tree = javalang.parse.parse(code) + try: + name = get_main_name(tree) + with open(os.path.join(path, f"{name}.java"), 'w') as f: + print(f"package p{contest_id}.p{submission_id};", file=f) + f.write(code) + except StopIteration: + print("Sleeping, because we reach request bound") + time.sleep(300) + + +def main(): + codeforces = "http://codeforces.com/api/" + api = CodeforcesAPI() + + with request.urlopen(f"{codeforces}problemset.problems") as req: + all_problems = check_json(req.read().decode('utf-8')) + + problems = [] + cur_problem = 0 + for p in all_problems['problems']: + if cur_problem >= args.problem_count: + break + if p.get('rating') is None: + continue + if p['rating'] < args.min_rating or p['rating'] > args.max_rating: + continue + cur_problem += 1 + problems.append({'contest_id': p['contestId'], 'index': p['index']}) + + print(f"Get {len(problems)} problems: {problems[0]}") + + """ + For each problem try to take submission_count submissions. + """ + all_submission = 0 + for i, p in enumerate(problems): + cur_submission = 0 + iteration = 0 + page_size = 1000 + while cur_submission < args.submission_count: + length = 0 + for s in api.contest_status(contest_id=p['contest_id'], from_=page_size * iteration + 1, count=page_size): + if cur_submission >= args.submission_count: + break + length += 1 + if s.problem.contest_id != p['contest_id'] or s.problem.index != p['index']: + continue + if s.programming_language != "Java 8": + continue + if s.verdict.name != "ok": + continue + save_source_code(p['contest_id'], s.id) + cur_submission += 1 + all_submission += 1 + print(f"Get new {all_submission} program") + iteration += 1 + if length == 0: + break + + +if __name__ == "__main__": + main() diff --git a/scripts/prepare.sh b/scripts/prepare.sh new file mode 100644 index 0000000000..74f10c74aa --- /dev/null +++ b/scripts/prepare.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +./gradlew clean build -x test + +INPUT_FOLDER=contest_input + +# Copy resources folder in distinct folder to allow other scripts have specific project in contest resources folder +mkdir $INPUT_FOLDER +cp -r utbot-junit-contest/src/main/resources/* $INPUT_FOLDER \ No newline at end of file diff --git a/scripts/prog_list b/scripts/prog_list new file mode 100644 index 0000000000..7ccf56032a --- /dev/null +++ b/scripts/prog_list @@ -0,0 +1 @@ +antlr diff --git a/scripts/quality_analysis.sh b/scripts/quality_analysis.sh new file mode 100644 index 0000000000..5d18d0e3a9 --- /dev/null +++ b/scripts/quality_analysis.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +PROJECT=${1} +SELECTORS=${2} +STMT_COVERAGE=${3} +WORKDIR="." + +# We set QualityAnalysisConfig by properties file +SETTING_PROPERTIES_FILE="$WORKDIR/utbot-analytics/src/main/resources/config.properties" +touch $SETTING_PROPERTIES_FILE +echo "project=$PROJECT" > "$SETTING_PROPERTIES_FILE" +echo "selectors=$SELECTORS" >> "$SETTING_PROPERTIES_FILE" + +JAR_TYPE="utbot-analytics" +echo "JAR_TYPE: $JAR_TYPE" +LIBS_DIR=utbot-analytics/build/libs/ +UTBOT_JAR="$LIBS_DIR$(ls -l $LIBS_DIR | grep $JAR_TYPE | awk '{print $9}')" +echo $UTBOT_JAR +MAIN_CLASS="org.utbot.QualityAnalysisKt" + +if [[ -n $STMT_COVERAGE ]]; then + MAIN_CLASS="org.utbot.StmtCoverageReportKt" +fi + + + +#Running the jar +COMMAND_LINE="java $JVM_OPTS -cp $UTBOT_JAR $MAIN_CLASS" + +echo "COMMAND=$COMMAND" + +$COMMAND_LINE diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 0000000000..3dcd5d56dc --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,7 @@ +beautifulsoup4 +javalang +numpy +pandas +requests +scikit_learn +torch diff --git a/scripts/run_contest_estimator.sh b/scripts/run_contest_estimator.sh new file mode 100644 index 0000000000..493bd51850 --- /dev/null +++ b/scripts/run_contest_estimator.sh @@ -0,0 +1,125 @@ +#!/bin/bash + +PROJECT=${1} +TIME_LIMIT=${2} +PATH_SELECTOR=${3} + +items=(${PATH_SELECTOR//##/ }) +PATH_SELECTOR_TYPE=${items[0]} +PATH_SELECTOR_PATH=${items[1]} +PREDICTOR_TYPE=${items[2]} +IS_COMBINED_SELECTOR=${items[3]} +ITERATIONS=${items[4]} + +echo "PATH_SELECTOR=$PATH_SELECTOR" +echo "PATH_SELECTOR_TYPE=$PATH_SELECTOR_TYPE" +echo "PATH_SELECTOR_PATH=$PATH_SELECTOR_PATH" + +FEATURE_ARG=${4} +featureItems=(${FEATURE_ARG//##/ }) +FEATURE_PROCESSING=${featureItems[0]} +FEATURE_PATH=${featureItems[1]} + +echo "FEATURE_ARG=$FEATURE_ARG" +echo "FEATURE_PATH=$FEATURE_PATH" + +COVERAGE_ARG=${5} +coverageItems=(${COVERAGE_ARG//##/ }) +COVERAGE_PROCESSING=${coverageItems[0]} +COVERAGE_PATH=${coverageItems[1]} + +WORKDIR="." +INPUT_FOLDER=contest_input + +# We set UtSettings by properties file +SETTING_PROPERTIES_FILE="$WORKDIR/settings.properties" +touch $SETTING_PROPERTIES_FILE +echo "pathSelectorType=$PATH_SELECTOR_TYPE" > "$SETTING_PROPERTIES_FILE" + +if [[ -n $PATH_SELECTOR_PATH ]]; then + echo "rewardModelPath=$PATH_SELECTOR_PATH" >> "$SETTING_PROPERTIES_FILE" +fi + +if [[ -n $IS_COMBINED_SELECTOR ]]; then + echo "singleSelector=false" >> "$SETTING_PROPERTIES_FILE" +fi + +if [[ -n $ITERATIONS ]]; then + echo "iterations=$ITERATIONS" >> "$SETTING_PROPERTIES_FILE" +fi + +if [[ -n $PREDICTOR_TYPE ]]; then + echo "nnStateRewardPredictorType=$PREDICTOR_TYPE" >> "$SETTING_PROPERTIES_FILE" +fi + +if [[ -n $FEATURE_PROCESSING ]]; then + echo "enableFeatureProcess=true" >> "$SETTING_PROPERTIES_FILE" + if [[ -z $FEATURE_PATH ]]; then + FEATURE_PATH=eval/features/$PATH_SELECTOR_TYPE/$PROJECT + fi + echo "featurePath=$FEATURE_PATH" >> "$SETTING_PROPERTIES_FILE" +fi + +if [[ -n $COVERAGE_PROCESSING ]]; then + echo "collectCoverage=true" >> "$SETTING_PROPERTIES_FILE" + echo "coverageStatisticsDir=$COVERAGE_PATH" >> "$SETTING_PROPERTIES_FILE" +fi + + +# Clean resources folder, because if there is more than one project, than there is may be error during jacoco report +RESOURCES_FOLDER="utbot-junit-contest/src/main/resources" +rm -rf $RESOURCES_FOLDER/classes/* +rm -rf $RESOURCES_FOLDER/projects/* +rm -rf $RESOURCES_FOLDER/evosuite + +# Copy target project in resources folder +cp -rp $INPUT_FOLDER/classes/$PROJECT $RESOURCES_FOLDER/classes/$PROJECT +cp -rp $INPUT_FOLDER/projects/$PROJECT $RESOURCES_FOLDER/projects/$PROJECT + +JAR_TYPE="utbot-junit-contest" +echo "JAR_TYPE: $JAR_TYPE" +LIBS_DIR="utbot-junit-contest/build/libs/" +UTBOT_JAR="$LIBS_DIR$(ls -l $LIBS_DIR | grep $JAR_TYPE | awk '{print $9}')" +MAIN_CLASS="org.utbot.contest.ContestEstimatorKt" +CLASSPATH=$RESOURCES_FOLDER/projects +echo "CLASS PATH: $CLASSPATH" +TARGET_CLASSES=$RESOURCES_FOLDER/classes +echo "TARGET CLASSES: $TARGET_CLASSES" +TIME_LIMIT_IN_SEC=$TIME_LIMIT +echo "TIME LIMIT IN SEC: $TIME_LIMIT_IN_SEC" +OUTPUT_DIR=$WORKDIR/utbot-junit-contest/build/output +echo "OUTPUT_DIR: $OUTPUT_DIR" +COMPILABLE_TESTS_TARGET_DIR=$OUTPUT_DIR/utbot-junit-contest/build/output +echo "COMPILABLE_TESTS_TARGET_DIR: $COMPILABLE_TESTS_TARGET_DIR" +JUNIT4_JAR="skip" +echo "JUNIT4_JAR: $JUNIT4_JAR" + +#JVM Flags and Options +JVM_OPTS="" +JVM_OPTS=$JVM_OPTS" -Xms512m" +JVM_OPTS=$JVM_OPTS" -Xmx12288m" +JVM_OPTS=$JVM_OPTS" -XX:+UseG1GC" +JVM_OPTS=$JVM_OPTS" -verbose:gc" +JVM_OPTS=$JVM_OPTS" -XX:+PrintGCDetails" +JVM_OPTS=$JVM_OPTS" -XX:+PrintGCTimeStamps" +JVM_OPTS=$JVM_OPTS" -Xloggc:$WORKDIR/run_contest_gc.log" + +JVM_OPTS=$JVM_OPTS" -Dutbot.settings.path=$SETTING_PROPERTIES_FILE" + +#Custom TMP directory - usually for server, change it by yours +#JVM_OPTS=$JVM_OPTS" -Djava.io.tmpdir=/home/wx1143086/tmp" + +echo "JVM_OPTS: $JVM_OPTS" + +#Creating output directory +mkdir -p $OUTPUT_DIR/test +rm -rf $OUTPUT_DIR/test/* + +echo "new directory is supposed to be created at %OUTPUT_DIR/test%" + +#Running the jar +COMMAND_LINE="java $JVM_OPTS -cp $UTBOT_JAR $MAIN_CLASS $TARGET_CLASSES $CLASSPATH $TIME_LIMIT_IN_SEC $OUTPUT_DIR $COMPILABLE_TESTS_TARGET_DIR $JUNIT4_JAR" + +set -o pipefail +echo "Command to run: $COMMAND_LINE 2>&1 | tee -a $WORKDIR/execution.out" +$COMMAND_LINE 2>&1 | tee -a $WORKDIR/execution.out \ No newline at end of file diff --git a/scripts/run_with_coverage.sh b/scripts/run_with_coverage.sh new file mode 100644 index 0000000000..29f41ddf20 --- /dev/null +++ b/scripts/run_with_coverage.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +PROJECT=${1} +TIME_LIMIT=${2} +PATH_SELECTOR=${3} +SELECTOR_ALIAS=${4} +COVERAGE_PROCESSING=${5} + + +if [[ -n $COVERAGE_PROCESSING ]]; then + COVERAGE_PROCESSING="true eval/report/${PROJECT}/${SELECTOR_ALIAS}" + mkdir -p "eval/report/${PROJECT}/${SELECTOR_ALIAS}" +fi + +WORKDIR="." +$WORKDIR/scripts/run_contest_estimator.sh $PROJECT $TIME_LIMIT "$PATH_SELECTOR" "" "$COVERAGE_PROCESSING" + +./gradlew :utbot-junit-contest:test :utbot-junit-contest:jacocoTestReport + +# Move jacoco report in some specific folder for future filter and analysis +OUTPUT_FOLDER=eval/jacoco/$PROJECT/$SELECTOR_ALIAS +rm -rf $OUTPUT_FOLDER +mkdir -p $OUTPUT_FOLDER +mv utbot-junit-contest/build/reports/jacoco/test/html/* "$OUTPUT_FOLDER"/ diff --git a/scripts/selector_list b/scripts/selector_list new file mode 100644 index 0000000000..c3344555d4 --- /dev/null +++ b/scripts/selector_list @@ -0,0 +1 @@ +RANDOM_SELECTOR diff --git a/scripts/train.py b/scripts/train.py new file mode 100644 index 0000000000..1f8735202a --- /dev/null +++ b/scripts/train.py @@ -0,0 +1,257 @@ +import argparse + +import pandas as pd +import os +import numpy as np +import sklearn.model_selection +import torch +import json +import sys +import copy + +from sklearn.linear_model import LinearRegression +from torch.optim.lr_scheduler import ReduceLROnPlateau +from torch.utils.data import TensorDataset, DataLoader +from sklearn.preprocessing import normalize, StandardScaler +from sklearn.metrics import mean_squared_error, mean_absolute_error + + +def get_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--features_dir", dest='features_dir', type=str) + parser.add_argument("--output_dir", dest='output_dir', type=str) + parser.add_argument("--prog_list", dest='prog_list', type=str) + parser.add_argument("--epochs", default=-1, type=int) + parser.add_argument("--device", default='cpu', type=str) + parser.add_argument("--batch_size", dest='batch_size', default=4096, type=int) + parser.add_argument("--hidden_dim", dest='hidden_dim', default=64, type=int) + parser.add_argument("--model", dest='model', default='nn', type=str) + + args = parser.parse_args() + args.device = 'cpu' if args.device == 'cpu' else f'cuda:{get_free_gpu()}' + return args + + +def get_free_gpu(): + """ + :return: index of gpu with maximum free memory + """ + os.system('nvidia-smi -q -d Memory |grep -A4 GPU|grep Free >tmp') + memory_available = [int(x.split()[2]) for x in open('tmp', 'r').readlines()] + return np.argmax(memory_available) + + +def get_data(args): + DATA_DIR = args.features_dir + + def get_data_selector(func_selector, acc_df): + for prog in progs: + if not os.path.exists(os.path.join(DATA_DIR, func_selector, prog)): + continue + for f in os.listdir(os.path.join(DATA_DIR, func_selector, prog)): + df = pd.read_csv(os.path.join(DATA_DIR, func_selector, prog, f)) + + if acc_df is None: + acc_df = df + else: + acc_df = pd.concat([acc_df, df]) + print(f"Finish collecting data with {func_selector}", flush=True) + return acc_df + + target_col = 'reward' + feature_cols = ['stack', 'successor', 'testCase', 'coverageByBranch', 'coverageByPath', 'depth', 'cpicnt', 'icnt', + 'covNew', 'subpath1', 'subpath2', 'subpath4', 'subpath8'] + progs = [] + with open(args.prog_list, 'r') as f: + for line in f: + progs.append(line[:-1]) + + total_df = None + for selector in os.listdir(DATA_DIR): + if selector.endswith("jlearch"): + continue + + total_df = get_data_selector(selector, total_df) + + """ + In jlearch directory we store features, which was collected by selectors using models, + which was trained on previous iterations. + We choose only features, that was collected by previous iteration of current model. + """ + jlearch_dir = os.path.join(DATA_DIR, "jlearch") + if os.path.exists(jlearch_dir): + for selector in os.listdir(jlearch_dir): + if not selector.startswith(args.model): + continue + + total_df = get_data_selector(selector, total_df) + + y = np.expand_dims(total_df[target_col].values, axis=1) + x = total_df[feature_cols].values + x = x.astype(float) + + return x, y + + +def dump_scaler(scaler, args): + """ + Dump scaler in such format: + - first line is mean vector values separated by comma + - second line is variance vector values separated by comma + """ + with open(os.path.join(args.output_dir, 'scaler.txt'), 'w') as f: + for array in np.vstack((scaler.mean_, scaler.scale_)).tolist(): + for item in array[:-1]: + f.write("%s," % item) + f.write("%s" % array[-1]) + f.write("\n") + + +class NeuralNetwork(torch.nn.Module): + def __init__(self, hidden_dim=64): + super(NeuralNetwork, self).__init__() + self.nn = torch.nn.Sequential( + torch.nn.Linear(13, hidden_dim), + torch.nn.ReLU(), + torch.nn.Linear(hidden_dim, hidden_dim), + torch.nn.ReLU(), + torch.nn.Linear(hidden_dim, 1) + ) + self.scaler = StandardScaler() + + def forward(self, x): + out = self.nn(x) + return out + + def do_train(self, x, y, args): + x = copy.deepcopy(x) + y = copy.deepcopy(y) + + x_train, x_test, y_train, y_test = sklearn.model_selection.train_test_split(x, y, test_size=0.2) + self.scaler = self.scaler.fit(x_train) + x_train = self.scaler.transform(x_train) + x_test = self.scaler.transform(x_test) + + learning_rate = 1e-3 + epochs = sys.maxsize if args.epochs == -1 else args.epochs + criterion = torch.nn.MSELoss() + optimizer = torch.optim.Adam(self.parameters(), lr=learning_rate, weight_decay=1e-5) + scheduler = ReduceLROnPlateau(optimizer, 'min', patience=3, verbose=True) + + tensor_x = torch.Tensor(x_train) + tensor_y = torch.Tensor(y_train) + tensor_x_test = torch.Tensor(x_test) + tensor_y_test = torch.Tensor(y_test) + + train_dataloader = DataLoader(TensorDataset(tensor_x, tensor_y), batch_size=args.batch_size) + test_dataloader = DataLoader(TensorDataset(tensor_x_test, tensor_y_test), batch_size=args.batch_size) + + for epoch in range(epochs): + if optimizer.param_groups[0]['lr'] <= 1e-6: + break + + train_loss = train_epoch(self, train_dataloader, criterion, optimizer, args.device) + val_loss, val_mae = eval_epoch(self, test_dataloader, criterion, args.device) + scheduler.step(val_loss) + + print('epoch {}, train_loss {}, val_loss {}, val_mae {}'.format(epoch, train_loss, val_loss, val_mae), + flush=True) + + def dump(self, args): + if not os.path.exists(args.output_dir): + os.makedirs(args.output_dir) + + dump_scaler(self.scaler, args) + + nn = { + "linearLayers": [], + "activationLayers": ["reLU", "reLU", "reLU"], + "biases": [], + } + + state_dict = self.cpu().state_dict() + for i in [0, 2, 4]: + nn["linearLayers"] += [state_dict[f'nn.{i}.weight'].numpy().tolist()] + nn["biases"] += [state_dict[f'nn.{i}.bias'].numpy().tolist()] + + with open(os.path.join(args.output_dir, 'nn.json'), 'w') as f: + json.dump(nn, f, indent=4) + + +class Linear(torch.nn.Module): + def __init__(self): + super(Linear, self).__init__() + self.model = LinearRegression() + self.scaler = StandardScaler() + + def do_train(self, x, y, args): + x = copy.deepcopy(x) + y = copy.deepcopy(y) + + self.scaler = self.scaler.fit(x) + self.model.fit(self.scaler.transform(x), y) + + def dump(self, args): + if not os.path.exists(args.output_dir): + os.makedirs(args.output_dir) + + dump_scaler(self.scaler, args) + with open(os.path.join(args.output_dir, 'linear.txt'), 'w') as f: + f.write(f"{','.join(map(str, self.model.coef_[0]))},{self.model.intercept_[0]}\n") + + +def train_epoch(model, data_loader, loss_fn, optimizer, device): + model.train(True) + + running_loss = 0.0 + processed_data = 0 + + for batch_x, batch_y in data_loader: + batch_x = batch_x.to(device=device) + batch_y = batch_y.to(device=device) + optimizer.zero_grad() + + outputs = model(batch_x) + loss = loss_fn(outputs, batch_y) + + running_loss += loss.detach().cpu().item() + processed_data += batch_x.shape[0] + + loss.backward() + optimizer.step() + + return running_loss / processed_data + + +def eval_epoch(model, data_loader, loss_fn, device): + model.eval() + + running_loss = 0.0 + running_ae = 0.0 + processed_data = 0 + + for batch_x, batch_y in data_loader: + batch_x = batch_x.to(device=device) + batch_y = batch_y.to(device=device) + outputs = model(batch_x) + loss = loss_fn(outputs, batch_y) + + running_loss += loss.detach().cpu().item() + processed_data += batch_x.shape[0] + running_ae += torch.sum(torch.abs(outputs - batch_y)) + + return running_loss / processed_data, running_ae / processed_data + + +def main(): + args = get_args() + model = NeuralNetwork(hidden_dim=args.hidden_dim) if ("nn" in args.model) else Linear() + model = model.to(device=args.device) + + x, y = get_data(args) + model.do_train(x, y, args) + model.dump(args) + + +if __name__ == "__main__": + main() diff --git a/scripts/train_data.sh b/scripts/train_data.sh new file mode 100644 index 0000000000..a31e8609fe --- /dev/null +++ b/scripts/train_data.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +WORKDIR="." +TIME_LIMIT=${1} + +while read prog; do + echo "Starting features collection from $prog" + prog="${prog%%[[:cntrl:]]}" + while read selector; do + echo "Starting features collection from $prog with $selector" + selector="${selector%%[[:cntrl:]]}" + $WORKDIR/scripts/run_contest_estimator.sh "$prog" "$TIME_LIMIT" "$selector" true + done <"$WORKDIR/scripts/selector_list" +done <"$WORKDIR/scripts/prog_list" diff --git a/scripts/train_iteratively.sh b/scripts/train_iteratively.sh new file mode 100644 index 0000000000..842fd771bd --- /dev/null +++ b/scripts/train_iteratively.sh @@ -0,0 +1,50 @@ +#!/bin/bash + +TIME_LIMIT=${1} +ITERATIONS=${2} +OUTPUT_DIR=${3} +PYTHON_COMMAND=${4} + +declare -a models=("linear" "nn16" "nn32" "nn64" "nn128") + +WORKDIR="." + +echo "Start training data on heuristical based selectors" + +$WORKDIR/scripts/train_data.sh $TIME_LIMIT + +echo "Start iterative learning of models" + +for (( i=0; i < $ITERATIONS; i++ )) +do + + echo "Start $i iteration" + + for model in "${models[@]}" + do + EXTRA_ARGS="" + if [[ $model == *"nn"* ]]; then + EXTRA_ARGS="--hidden_dim $(echo $model | cut -c 3-)" + echo "EXTRA_ARGS=$EXTRA_ARGS" + fi + + COMMAND="$PYTHON_COMMAND $WORKDIR/scripts/train.py --features_dir $WORKDIR/eval/features --output_dir $OUTPUT_DIR/$model/$i --prog_list $WORKDIR/scripts/prog_list --model $model $EXTRA_ARGS" + echo "TRAINING COMMAND=$COMMAND" + $COMMAND + done + + while read prog; do + prog="${prog%%[[:cntrl:]]}" + + for model in "${models[@]}" + do + PREDICTOR="BASE" + + if [[ $model == *"linear"* ]]; then + PREDICTOR="LINEAR" + fi + + $WORKDIR/scripts/run_contest_estimator.sh $prog $TIME_LIMIT "NN_REWARD_GUIDED_SELECTOR $OUTPUT_DIR/$model/$i $PREDICTOR" "true eval/features/jlearch/$model$i/$prog" + done + done <"$WORKDIR/scripts/prog_list" +done diff --git a/settings.gradle b/settings.gradle index 87a0b942ee..ac2c9d1279 100644 --- a/settings.gradle +++ b/settings.gradle @@ -15,4 +15,6 @@ include 'utbot-instrumentation-tests' include 'utbot-summary' include 'utbot-gradle' +include 'utbot-maven' +include 'utbot-summary-tests' diff --git a/utbot-analytics/build.gradle b/utbot-analytics/build.gradle index 3c06d3cc17..2757b9f65d 100644 --- a/utbot-analytics/build.gradle +++ b/utbot-analytics/build.gradle @@ -1,17 +1,19 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" -apply plugin: "java" configurations { mlmodels } +def osName = System.getProperty('os.name').toLowerCase().split()[0] +if (osName == "mac") osName = "macosx" +String classifier = osName + "-x86_64" -String classifier = System.getProperty('os.name').toLowerCase().split()[0] + "-x86_64" evaluationDependsOn(':utbot-framework') compileTestJava.dependsOn tasks.getByPath(':utbot-framework:testClasses') + dependencies { implementation(project(":utbot-framework")) - implementation(project(':utbot-instrumentation')) + compile(project(':utbot-instrumentation')) implementation(project(':utbot-summary')) testImplementation project(':utbot-sample') testImplementation group: 'junit', name: 'junit', version: junit4_version @@ -26,6 +28,7 @@ dependencies { implementation group: 'org.bytedeco', name: 'arpack-ng', version: "3.7.0-1.5.4", classifier: "$classifier" implementation group: 'org.bytedeco', name: 'openblas', version: "0.3.10-1.5.4", classifier: "$classifier" + implementation group: 'org.bytedeco', name: 'javacpp', version: javacpp_version, classifier: "$classifier" implementation group: 'tech.tablesaw', name: 'tablesaw-core', version: '0.38.2' implementation group: 'tech.tablesaw', name: 'tablesaw-jsplot', version: '0.38.2' @@ -34,15 +37,19 @@ dependencies { implementation group: 'com.github.javaparser', name: 'javaparser-core', version: '3.22.1' + implementation group: 'org.jsoup', name: 'jsoup', version: jsoup_version + + implementation "ai.djl:api:$djl_api_version" + implementation "ai.djl.pytorch:pytorch-engine:$djl_api_version" + implementation "ai.djl.pytorch:pytorch-native-auto:$pytorch_native_version" + testCompile project(':utbot-framework').sourceSets.test.output } test { - - useJUnitPlatform{ - excludeTags 'Summary' + useJUnitPlatform { + excludeTags 'Summary' } - } processResources { @@ -51,4 +58,17 @@ processResources { into "models" } } +} + +jar { + dependsOn classes + manifest { + attributes 'Main-Class': 'org.utbot.QualityAnalysisKt' + } + + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + + duplicatesStrategy = DuplicatesStrategy.EXCLUDE } \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorFactoryImpl.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorFactoryImpl.kt new file mode 100644 index 0000000000..9def22a3dc --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorFactoryImpl.kt @@ -0,0 +1,12 @@ +package org.utbot.features + +import org.utbot.analytics.FeatureExtractor +import org.utbot.analytics.FeatureExtractorFactory +import org.utbot.engine.InterProceduralUnitGraph + +/** + * Implementation of feature extractor factory + */ +class FeatureExtractorFactoryImpl : FeatureExtractorFactory { + override operator fun invoke(graph: InterProceduralUnitGraph): FeatureExtractor = FeatureExtractorImpl(graph) +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorImpl.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorImpl.kt new file mode 100644 index 0000000000..6786c9847a --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureExtractorImpl.kt @@ -0,0 +1,50 @@ +package org.utbot.features + +import org.utbot.analytics.FeatureExtractor +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.selectors.strategies.StatementsStatistics +import org.utbot.engine.selectors.strategies.SubpathStatistics + +/** + * Implementation of feature extractor. + * Extract features for state and stores it in features vector of this state. + * + * @param graph execution graph of current symbolic traverse + */ +class FeatureExtractorImpl(private val graph: InterProceduralUnitGraph) : FeatureExtractor { + companion object { + /** + * Indexes for [SubpathStatistics], with which we want to collect our features + */ + private val subpathGuidedSelectorIndexes = listOf(0, 1, 2, 3) + } + + private fun MutableList.add(value: T) = add(value.toDouble()) + private fun > MutableList.add(value: T) = add(value.size) + + private val subpathStatistics = subpathGuidedSelectorIndexes.map { SubpathStatistics(graph, it) } + private val statementStatistics = StatementsStatistics(graph) + + override fun extractFeatures(executionState: ExecutionState, generatedTestCases: Int) { + with(executionState.features) { + if (isNotEmpty()) { + clear() + } + + add(executionState.executionStack) // stack + add(graph.succs(executionState.stmt)) // successor + add(generatedTestCases) // testCase + add(executionState.visitedAfterLastFork) // coverage by branch + add(executionState.visitedBeforeLastFork + executionState.visitedAfterLastFork) // coverage by path + add(executionState.depth) // depth + add(statementStatistics.statementInMethodCount(executionState)) // cpicnt + add(statementStatistics.statementCount(executionState)) // icnt + add(executionState.stmtsSinceLastCovered) // covNew + + subpathStatistics.forEach { + add(it.subpathCount(executionState)) // sgs_i + } + } + } +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt new file mode 100644 index 0000000000..49f9427678 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetition.kt @@ -0,0 +1,162 @@ +package org.utbot.features + +import org.utbot.analytics.EngineAnalyticsContext +import org.utbot.analytics.FeatureProcessor +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.framework.UtSettings +import soot.jimple.Stmt +import java.io.File +import java.io.FileOutputStream +import java.nio.file.Paths +import kotlin.math.pow + +/** + * Implementation of feature processor, in which we dump each test, so there will be several copies of each state. + * Goal is make weighted dataset, where more value for states, which generated more tests. + * Extract features for state when this state will be marked visited in graph. + * Add test case, when last state of it will be traversed. + * + * @param graph execution graph of current symbolic traverse + * @param saveDir directory in which we will store features and rewards of [ExecutionState] + */ +class FeatureProcessorWithStatesRepetition( + graph: InterProceduralUnitGraph, + private val saveDir: String = UtSettings.featurePath +) : FeatureProcessor(graph) { + init { + File(saveDir).mkdirs() + } + + companion object { + private const val featureFile = "jlearch_features" + private val featureKeys = Companion::class.java.classLoader.getResourceAsStream(featureFile) + ?.bufferedReader().use { + it?.readText()?.split(System.lineSeparator()) ?: emptyList() + } + } + + private var generatedTestCases = 0 + private val featureExtractor = EngineAnalyticsContext.featureExtractorFactory(graph) + private val rewardEstimator = RewardEstimator() + + private val dumpedStates = mutableMapOf>() + private val visitedStmts = mutableSetOf() + private val testCases = mutableListOf() + + private fun extractFeatures(executionState: ExecutionState) { + featureExtractor.extractFeatures(executionState, generatedTestCases) + } + + private fun addTestCase(executionState: ExecutionState) { + val states = mutableListOf>() + var newCoverage = 0 + + generateSequence(executionState) { it.parent }.forEach { currentState -> + val stateHashCode = currentState.hashCode() + + if (currentState.features.isEmpty()) { + extractFeatures(currentState) + } + + states += stateHashCode to currentState.executingTime + dumpedStates[stateHashCode] = currentState.features + + currentState.stmt.let { + if (it !in visitedStmts && !currentState.isInNestedMethod()) { + visitedStmts += it + newCoverage++ + } + } + } + + generatedTestCases++ + testCases += TestCase(states, newCoverage, generatedTestCases) + } + + override fun dumpFeatures() { + val rewards = rewardEstimator.calculateRewards(testCases) + + testCases.forEach { ts -> + val outputFile = Paths.get(saveDir, "${UtSettings.testCounter++}.csv").toFile() + FileOutputStream(outputFile, true) + .bufferedWriter() + .use { out -> + out.appendLine("newCov,reward,${featureKeys.joinToString(separator = ",")}") + val reversedStates = ts.states.asReversed() + + reversedStates.forEach { (state, _) -> + val isCoveredNew = ts.newCoverage != 0 + val reward = rewards[state] + val features = dumpedStates[state]?.joinToString(separator = ",") + + out.appendLine("$isCoveredNew,$reward,$features") + } + + out.flush() + } + } + } + + override fun onTraversed(executionState: ExecutionState) { + addTestCase(executionState) + } + + override fun onVisit(executionState: ExecutionState) { + extractFeatures(executionState) + } +} + +internal class RewardEstimator { + + fun calculateRewards(testCases: List): Map { + val rewards = mutableMapOf() + val coverages = mutableMapOf() + val stateToExecutingTime = mutableMapOf() + + testCases.forEach { ts -> + var allTime = 0L + ts.states.forEach { (stateHash, time) -> + coverages.compute(stateHash) { _, v -> + ts.newCoverage + (v ?: 0) + } + val isNewState = stateHash !in stateToExecutingTime + stateToExecutingTime.compute(stateHash) { _, v -> + allTime + (v ?: time) + } + if (isNewState) { + allTime += time + } + } + } + + coverages.forEach { (state, coverage) -> + rewards[state] = reward(coverage.toDouble(), stateToExecutingTime.getValue(state).toDouble()) + } + + return rewards + } + + companion object { + /** + * Threshold for time: executingTime less than that we don't distinct. We are not expiremented with changing it yet, + * now it is just minimal positive value distinct from 0. + */ + private const val minTime = 1.0 + + /** + * Just degree of reward to make it smaller if it more than 1 and bigger if it less than 1. + */ + private const val rewardDegree = 0.5 + + fun reward(coverage: Double, time: Double): Double = (coverage / maxOf(time, minTime)).pow(rewardDegree) + } +} + +/** + * Class that represents test case. + * @param states pairs from stateHash and executingTime, created from each state of this test case + * @param newCoverage number of instructions, that was visited in first time by [states] + * @param testIndex number of test case, that was created before + */ +data class TestCase(val states: List>, val newCoverage: Int, val testIndex: Int) diff --git a/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetitionFactory.kt b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetitionFactory.kt new file mode 100644 index 0000000000..51744e6918 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/features/FeatureProcessorWithStatesRepetitionFactory.kt @@ -0,0 +1,13 @@ +package org.utbot.features + +import org.utbot.analytics.FeatureProcessor +import org.utbot.analytics.FeatureProcessorFactory +import org.utbot.engine.InterProceduralUnitGraph + +/** + * Implementation of feature processor factory, which creates FeatureProcessorWithStatesRepetition. + * See [FeatureProcessorWithStatesRepetition]. + */ +class FeatureProcessorWithStatesRepetitionFactory : FeatureProcessorFactory { + override fun invoke(graph: InterProceduralUnitGraph): FeatureProcessor = FeatureProcessorWithStatesRepetition(graph) +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/FeedForwardNetwork.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/FeedForwardNetwork.kt new file mode 100644 index 0000000000..3e0dfbb9f7 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/FeedForwardNetwork.kt @@ -0,0 +1,37 @@ +package org.utbot.predictors + +import org.utbot.predictors.util.ModelBuildingException +import smile.math.matrix.Matrix +import kotlin.math.max + +private object ActivationFunctions { + const val ReLU = "reLU" +} + +data class FeedForwardNetwork(val operations: List<(DoubleArray) -> DoubleArray>) + +private fun reLU(input: DoubleArray): DoubleArray { + return input.map { max(0.0, it) }.toDoubleArray() +} + +internal fun buildModel(nnJson: NNJson): FeedForwardNetwork { + val weights = nnJson.linearLayers.map { Matrix(it) } + val biases = nnJson.biases.map { Matrix(it) } + val operations = mutableListOf<(DoubleArray) -> DoubleArray>() + + nnJson.linearLayers.indices.forEach { i -> + operations.add { + weights[i].mm(Matrix(it)).add(biases[i]).col(0) + } + if (i != nnJson.linearLayers.lastIndex) { + operations.add { + when (nnJson.activationLayers[i]) { + ActivationFunctions.ReLU -> reLU(it) + else -> throw ModelBuildingException("Unsupported activation") + } + } + } + } + + return FeedForwardNetwork(operations) +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/LinearStateRewardPredictor.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/LinearStateRewardPredictor.kt new file mode 100644 index 0000000000..2d3dc434de --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/LinearStateRewardPredictor.kt @@ -0,0 +1,72 @@ +package org.utbot.predictors + +import org.utbot.analytics.StateRewardPredictor +import mu.KotlinLogging +import org.utbot.framework.PathSelectorType +import org.utbot.framework.UtSettings +import org.utbot.predictors.util.PredictorLoadingException +import org.utbot.predictors.util.WeightsLoadingException +import org.utbot.predictors.util.splitByCommaIntoDoubleArray +import smile.math.MathEx.dot +import smile.math.matrix.Matrix +import java.io.File + +private const val DEFAULT_WEIGHT_PATH = "linear.txt" + +private val logger = KotlinLogging.logger {} + +/** + * Last weight is bias + */ +private fun loadWeights(path: String): Matrix { + val weightsFile = File("${UtSettings.rewardModelPath}/${path}") + lateinit var weightsArray: DoubleArray + + try { + if (!weightsFile.exists()) { + error("There is no file with weights with path: ${weightsFile.absolutePath}") + } + + weightsArray = weightsFile.readText().splitByCommaIntoDoubleArray() + } catch (e: Exception) { + throw WeightsLoadingException(e) + } + + return Matrix(weightsArray) +} + +class LinearStateRewardPredictor(weightsPath: String = DEFAULT_WEIGHT_PATH, scalerPath: String = DEFAULT_SCALER_PATH) : + StateRewardPredictor { + private lateinit var weights: Matrix + private lateinit var scaler: StandardScaler + + init { + try { + weights = loadWeights(weightsPath) + scaler = loadScaler(scalerPath) + } catch (e: PredictorLoadingException) { + logger.info(e) { + "Error while initialization of LinearStateRewardPredictor. Changing pathSelectorType on INHERITORS_SELECTOR" + } + UtSettings.pathSelectorType = PathSelectorType.INHERITORS_SELECTOR + } + } + + fun predict(input: List>): List { + // add 1 to each feature vector + val matrixValues = input + .map { (it + 1.0).toDoubleArray() } + .toTypedArray() + + val X = Matrix(matrixValues) + + return X.mm(weights).col(0).toList() + } + + override fun predict(input: List): Double { + var inputArray = Matrix(input.toDoubleArray()).sub(scaler.mean).div(scaler.variance).col(0) + inputArray += 1.0 + + return dot(inputArray, weights.col(0)) + } +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNJson.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNJson.kt new file mode 100644 index 0000000000..d14034e64d --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNJson.kt @@ -0,0 +1,49 @@ +package org.utbot.predictors + +import com.google.gson.Gson +import org.utbot.framework.UtSettings +import org.utbot.predictors.util.ModelLoadingException +import java.io.FileReader +import java.nio.file.Paths + +data class NNJson( + val linearLayers: Array> = emptyArray(), + val activationLayers: Array = emptyArray(), + val biases: Array = emptyArray() +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as NNJson + + if (!linearLayers.contentDeepEquals(other.linearLayers)) return false + if (!activationLayers.contentEquals(other.activationLayers)) return false + if (!biases.contentDeepEquals(other.biases)) return false + + return true + } + + override fun hashCode(): Int { + var result = linearLayers.contentDeepHashCode() + result = 31 * result + activationLayers.contentHashCode() + result = 31 * result + biases.contentDeepHashCode() + return result + } +} + +internal fun loadModel(path: String): NNJson { + val modelFile = Paths.get(UtSettings.rewardModelPath, path).toFile() + lateinit var nnJson: NNJson + + try { + nnJson = + Gson().fromJson(FileReader(modelFile), NNJson::class.java) ?: run { + error("Empty model") + } + } catch (e: Exception) { + throw ModelLoadingException(e) + } + + return nnJson +} diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNStateRewardPredictorBase.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNStateRewardPredictorBase.kt new file mode 100644 index 0000000000..d8e77c4d23 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/NNStateRewardPredictorBase.kt @@ -0,0 +1,44 @@ +package org.utbot.predictors + +import mu.KotlinLogging +import org.utbot.analytics.StateRewardPredictor +import org.utbot.framework.PathSelectorType +import org.utbot.framework.UtSettings +import org.utbot.predictors.util.PredictorLoadingException +import smile.math.matrix.Matrix + +private const val DEFAULT_MODEL_PATH = "nn.json" + +private val logger = KotlinLogging.logger {} + +private fun getModel(path: String) = buildModel(loadModel(path)) + +class NNStateRewardPredictorBase(modelPath: String = DEFAULT_MODEL_PATH, scalerPath: String = DEFAULT_SCALER_PATH) : + StateRewardPredictor { + private lateinit var nn: FeedForwardNetwork + private lateinit var scaler: StandardScaler + + init { + try { + nn = getModel(modelPath) + scaler = loadScaler(scalerPath) + } catch (e: PredictorLoadingException) { + logger.info(e) { + "Error while initialization of NNStateRewardPredictorBase. Changing pathSelectorType on INHERITORS_SELECTOR" + } + UtSettings.pathSelectorType = PathSelectorType.INHERITORS_SELECTOR + } + } + + override fun predict(input: List): Double { + var inputArray = input.toDoubleArray() + inputArray = Matrix(inputArray).sub(scaler.mean).div(scaler.variance).col(0) + + nn.operations.forEach { + inputArray = it(inputArray) + } + + check(inputArray.size == 1) { "Neural network have several outputs" } + return inputArray[0] + } +} diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorFactory.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorFactory.kt new file mode 100644 index 0000000000..c0b7dffe7c --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorFactory.kt @@ -0,0 +1,16 @@ +package org.utbot.predictors + +import org.utbot.analytics.StateRewardPredictorFactory +import org.utbot.framework.StateRewardPredictorType +import org.utbot.framework.UtSettings + +/** + * Creates [StateRewardPredictor], by checking the [UtSettings] configuration. + */ +class StateRewardPredictorFactoryImpl : StateRewardPredictorFactory { + override operator fun invoke() = when (UtSettings.stateRewardPredictorType) { + StateRewardPredictorType.BASE -> NNStateRewardPredictorBase() + StateRewardPredictorType.TORCH -> StateRewardPredictorTorch() + StateRewardPredictorType.LINEAR -> LinearStateRewardPredictor() + } +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorTorch.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorTorch.kt new file mode 100644 index 0000000000..f0cfe17571 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/StateRewardPredictorTorch.kt @@ -0,0 +1,38 @@ +package org.utbot.predictors + +import ai.djl.Model +import ai.djl.inference.Predictor +import ai.djl.ndarray.NDArray +import ai.djl.ndarray.NDList +import ai.djl.translate.Translator +import ai.djl.translate.TranslatorContext +import org.utbot.analytics.StateRewardPredictor +import org.utbot.framework.UtSettings +import java.io.Closeable +import java.nio.file.Paths + +class StateRewardPredictorTorch : StateRewardPredictor, Closeable { + val model: Model = Model.newInstance("model") + + init { + model.load(Paths.get(UtSettings.rewardModelPath, "model.pt1")) + } + + private val predictor: Predictor, Float> = model.newPredictor(object : Translator, Float> { + override fun processInput(ctx: TranslatorContext, input: List): NDList { + val array: NDArray = ctx.ndManager.create(input.toFloatArray()) + return NDList(array) + } + + override fun processOutput(ctx: TranslatorContext, list: NDList): Float = list[0].getFloat() + }) + + override fun predict(input: List): Double { + val reward: Float = predictor.predict(input.map { it.toFloat() }.toList()) + return reward.toDouble() + } + + override fun close() { + predictor.close() + } +} \ No newline at end of file diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/scalers.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/scalers.kt new file mode 100644 index 0000000000..b65e78d478 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/scalers.kt @@ -0,0 +1,23 @@ +package org.utbot.predictors + +import org.utbot.framework.UtSettings +import org.utbot.predictors.util.ScalerLoadingException +import org.utbot.predictors.util.splitByCommaIntoDoubleArray +import smile.math.matrix.Matrix +import java.nio.file.Paths + + +internal const val DEFAULT_SCALER_PATH = "scaler.txt" + +data class StandardScaler(val mean: Matrix?, val variance: Matrix?) + +internal fun loadScaler(path: String): StandardScaler = + try { + Paths.get(UtSettings.rewardModelPath, path).toFile().bufferedReader().use { + val mean = it.readLine()?.splitByCommaIntoDoubleArray() ?: error("There is not mean in $path") + val variance = it.readLine()?.splitByCommaIntoDoubleArray() ?: error("There is not variance in $path") + StandardScaler(Matrix(mean), Matrix(variance)) + } + } catch (e: Exception) { + throw ScalerLoadingException(e) + } diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/PredictorLoadingException.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/PredictorLoadingException.kt new file mode 100644 index 0000000000..d45e7dec8d --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/PredictorLoadingException.kt @@ -0,0 +1,11 @@ +package org.utbot.predictors.util + +sealed class PredictorLoadingException(msg: String?, cause: Throwable? = null) : Exception(msg, cause) + +class WeightsLoadingException(e: Throwable) : PredictorLoadingException("Error while loading weights", e) + +class ModelLoadingException(e: Throwable) : PredictorLoadingException("Error while loading model", e) + +class ScalerLoadingException(e: Throwable) : PredictorLoadingException("Error while loading scaler", e) + +class ModelBuildingException(msg: String) : PredictorLoadingException(msg) diff --git a/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/StringUtils.kt b/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/StringUtils.kt new file mode 100644 index 0000000000..b6f3c98e53 --- /dev/null +++ b/utbot-analytics/src/main/kotlin/org/utbot/predictors/util/StringUtils.kt @@ -0,0 +1,8 @@ +package org.utbot.predictors.util + +fun String.splitByCommaIntoDoubleArray() = + try { + split(',').map(String::toDouble).toDoubleArray() + } catch (e: NumberFormatException) { + error("Wrong format in $this, expect doubles separated by commas") + } \ No newline at end of file diff --git a/utbot-analytics/src/main/resources/jlearch_features b/utbot-analytics/src/main/resources/jlearch_features new file mode 100644 index 0000000000..0a852928bf --- /dev/null +++ b/utbot-analytics/src/main/resources/jlearch_features @@ -0,0 +1,13 @@ +stack +successor +testCase +coverageByBranch +coverageByPath +depth +cpicnt +icnt +covNew +subpath1 +subpath2 +subpath4 +subpath8 \ No newline at end of file diff --git a/utbot-analytics/src/test/java/org/utbot/features/OnePath.java b/utbot-analytics/src/test/java/org/utbot/features/OnePath.java new file mode 100644 index 0000000000..15bd5a8786 --- /dev/null +++ b/utbot-analytics/src/test/java/org/utbot/features/OnePath.java @@ -0,0 +1,10 @@ +package org.utbot.features; + +/** + * Class that have one execution path + */ +public class OnePath { + int onePath() { + return 1; + } +} diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/algorithms/SummaryReturnExampleTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/algorithms/SummaryReturnExampleTest.kt deleted file mode 100644 index 7d5e23e262..0000000000 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/algorithms/SummaryReturnExampleTest.kt +++ /dev/null @@ -1,363 +0,0 @@ -package org.utbot.analytics.examples.algorithms - -import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest -import org.utbot.examples.algorithms.ReturnExample -import org.junit.jupiter.api.Test - -class SummaryReturnExampleTest : SummaryTestCaseGeneratorTest( - ReturnExample::class, -) { - - val summaryCompare1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): True}\n" +
-            "returns from:\n" +
-            "    1st return statement: {@code return a;}\n" +
-            "
" - val summaryCompare2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): False},\n" +
-            "    {@code (b < 0): True}\n" +
-            "returns from:\n" +
-            "    1st return statement: {@code return a;}\n" +
-            "
" - val summaryCompare3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): False},\n" +
-            "    {@code (b < 0): False},\n" +
-            "    {@code (b == 10): True}\n" +
-            "returns from:\n" +
-            "    1st return statement: {@code return c;}\n" +
-            "
" - val summaryCompare4 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): False},\n" +
-            "    {@code (b < 0): False},\n" +
-            "    {@code (b == 10): False},\n" +
-            "    {@code (a > b): False},\n" +
-            "    {@code (a < b): True}\n" +
-            "returns from:\n" +
-            "    2nd return statement: {@code return a;}\n" +
-            "
" - val summaryCompare5 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): False},\n" +
-            "    {@code (b < 0): False},\n" +
-            "    {@code (b == 10): False},\n" +
-            "    {@code (a > b): True}\n" +
-            "returns from: {@code return b;}\n" +
-            "
" - val summaryCompare6 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (a < 0): False},\n" +
-            "    {@code (b < 0): False},\n" +
-            "    {@code (b == 10): False},\n" +
-            "    {@code (a > b): False},\n" +
-            "    {@code (a < b): False}\n" +
-            "returns from:\n" +
-            "    2nd return statement: {@code return c;}\n" +
-            "
" - - @Test - fun testCompare() { - checkTwoArguments( - ReturnExample::compare, - summaryKeys = listOf( - summaryCompare1, - summaryCompare2, - summaryCompare3, - summaryCompare4, - summaryCompare5, - summaryCompare6 - ), - displayNames = listOf( - "a < 0 : False -> return a", - "b < 0 : True -> return a", - "b == 10 : True -> return c", - "a < b : True -> return a", - "a > b : True -> return b", - "a < b : False -> return c" - ) - ) - } - - val summaryCompareChars1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (n < 1): True}\n" +
-            "returns from: {@code return ' ';}\n" +
-            "
" - val summaryCompareChars2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (n < 1): False}\n" +
-            "iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (Character.toChars(i)[0] == a): True}\n" +
-            "returns from: {@code return b;}\n" +
-            "
" - val summaryCompareChars3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (n < 1): False}\n" +
-            "iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (Character.toChars(i)[0] == a): False},\n" +
-            "    {@code (Character.toChars(i)[0] == b): True}\n" +
-            "returns from:\n" +
-            "    1st return statement: {@code return a;}\n" +
-            "
" - val summaryCompareChars4 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (n < 1): False}\n" +
-            "iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (Character.toChars(i)[0] == a): False},\n" +
-            "    {@code (Character.toChars(i)[0] == b): False}"
-
-    @Test
-    fun testCompareChar() {
-        checkThreeArguments(
-            ReturnExample::compareChars,
-            summaryKeys = listOf(
-                summaryCompareChars1,
-                summaryCompareChars2,
-                summaryCompareChars3,
-                summaryCompareChars4
-            ),
-            displayNames = listOf(
-                "n < 1 : True -> return ' '",
-                "Character.toChars(i)[0] == a : True -> return b",
-                "Character.toChars(i)[0] == b : True -> return a",
-                "Character.toChars(i)[0] == b : False -> return a"
-            )
-        )
-    }
-
-    val summaryVoidCompareChars1 = "
\n" +
-            "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): True}\n" +
-            "    returns from: {@code return ' ';}\n" +
-            "
" - val summaryVoidCompareChars2 = "
\n" +
-            "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): False}\n" +
-            "    iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "        {@code (Character.toChars(i)[0] == a): True}\n" +
-            "    returns from: {@code return b;}\n" +
-            "
" - val summaryVoidCompareChars3 = "
\n" +
-            "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): False}\n" +
-            "    iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "        {@code (Character.toChars(i)[0] == a): False},\n" +
-            "        {@code (Character.toChars(i)[0] == b): True}\n" +
-            "    returns from: {@code return a;}\n" +
-            "
" - val summaryVoidCompareChars4 = "Test calls ReturnExample::compareChars,\n" + - " there it executes conditions:\n" + - " {@code (n < 1): False}\n" + - " iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (Character.toChars(i)[0] == a): False},\n" + - " {@code (Character.toChars(i)[0] == b): False}" - - - @Test - fun testInnerVoidCompareChars() { - checkThreeArguments( - ReturnExample::innerVoidCompareChars, - summaryKeys = listOf( - summaryVoidCompareChars1, - summaryVoidCompareChars2, - summaryVoidCompareChars3, - summaryVoidCompareChars4 - ), - displayNames = listOf( - "n < 1 : True -> return ' '", - "Character.toChars(i)[0] == a : True -> return b", - "Character.toChars(i)[0] == b : True -> return a", - "Character.toChars(i)[0] == b : False -> return a" - ) - ) - } - - val summaryInnerReturnCompareChars1 = "
\n" +
-            "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): True}\n" +
-            "    returns from: {@code return ' ';}"
-    val summaryInnerReturnCompareChars2 = "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): False}\n" +
-            "    iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "        {@code (Character.toChars(i)[0] == a): True}\n" +
-            "    returns from: {@code return b;}"
-    val summaryInnerReturnCompareChars3 = "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): False}\n" +
-            "    iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "        {@code (Character.toChars(i)[0] == a): False},\n" +
-            "        {@code (Character.toChars(i)[0] == b): True}\n" +
-            "    returns from: {@code return a;}"
-    val summaryInnerReturnCompareChars4 = "Test calls ReturnExample::compareChars,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (n < 1): False}\n" +
-            "    iterates the loop {@code for(int i = 0; i < n; i++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "        {@code (Character.toChars(i)[0] == a): False},\n" +
-            "        {@code (Character.toChars(i)[0] == b): False}"
-
-    @Test
-    fun testInnerReturnCompareChars() {
-        checkThreeArguments(
-            ReturnExample::innerReturnCompareChars,
-            summaryKeys = listOf(
-                summaryInnerReturnCompareChars1,
-                summaryInnerReturnCompareChars2,
-                summaryInnerReturnCompareChars3,
-                summaryInnerReturnCompareChars4
-            )
-        )
-    }
-
-    val summaryInnerVoidCompare1 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): False},\n" +
-            "        {@code (b < 0): True}\n" +
-            "    returns from: {@code return a;}\n" +
-            "
" - val summaryInnerVoidCompare2 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): False},\n" +
-            "        {@code (b < 0): False},\n" +
-            "        {@code (b == 10): True}\n" +
-            "    returns from: {@code return c;}\n" +
-            "
" - val summaryInnerVoidCompare3 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): False},\n" +
-            "        {@code (b < 0): False},\n" +
-            "        {@code (b == 10): False},\n" +
-            "        {@code (a > b): False},\n" +
-            "        {@code (a < b): False}\n" +
-            "    returns from: {@code return c;}\n" +
-            "
" - val summaryInnerVoidCompare4 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): False},\n" +
-            "        {@code (b < 0): False},\n" +
-            "        {@code (b == 10): False},\n" +
-            "        {@code (a > b): False},\n" +
-            "        {@code (a < b): True}\n" +
-            "    returns from: {@code return a;}\n" +
-            "
" - val summaryInnerVoidCompare5 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): True}\n" +
-            "    returns from: {@code return a;}\n" +
-            "
" - val summaryInnerVoidCompare6 = "
\n" +
-            "Test calls ReturnExample::compare,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (a < 0): False},\n" +
-            "        {@code (b < 0): False},\n" +
-            "        {@code (b == 10): False},\n" +
-            "        {@code (a > b): True}\n" +
-            "    returns from: {@code return b;}\n" +
-            "
" - - @Test - fun testInnerVoidCompare() { - checkTwoArguments( - ReturnExample::innerVoidCallCompare, - summaryKeys = listOf( - summaryInnerVoidCompare1, - summaryInnerVoidCompare2, - summaryInnerVoidCompare3, - summaryInnerVoidCompare4, - summaryInnerVoidCompare5, - summaryInnerVoidCompare6 - ), - displayNames = listOf( - "b < 0 : True -> return a", - "b == 10 : True -> return c", - "a < b : False -> return c", - "a < b : True -> return a", - "a < 0 : False -> return a", - "a > b : True -> return b" - ) - ) - } - - val summaryInnerReturnCompare1 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): False},\n" + - " {@code (b < 0): True}\n" + - " returns from: {@code return a;}" - val summaryInnerReturnCompare2 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): False},\n" + - " {@code (b < 0): False},\n" + - " {@code (b == 10): True}\n" + - " returns from: {@code return c;}" - val summaryInnerReturnCompare3 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): False},\n" + - " {@code (b < 0): False},\n" + - " {@code (b == 10): False},\n" + - " {@code (a > b): False},\n" + - " {@code (a < b): False}\n" + - " returns from: {@code return c;}" - val summaryInnerReturnCompare4 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): False},\n" + - " {@code (b < 0): False},\n" + - " {@code (b == 10): False},\n" + - " {@code (a > b): False},\n" + - " {@code (a < b): True}\n" + - " returns from: {@code return a;}" - val summaryInnerReturnCompare5 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): True}\n" + - " returns from: {@code return a;}" - val summaryInnerReturnCompare6 = "Test calls ReturnExample::compare,\n" + - " there it executes conditions:\n" + - " {@code (a < 0): False},\n" + - " {@code (b < 0): False},\n" + - " {@code (b == 10): False},\n" + - " {@code (a > b): True}\n" + - " returns from: {@code return b;}" - - @Test - fun testInnerReturnCompare() { - checkTwoArguments( - ReturnExample::innerReturnCallCompare, - summaryKeys = listOf( - summaryInnerReturnCompare1, - summaryInnerReturnCompare2, - summaryInnerReturnCompare3, - summaryInnerReturnCompare4, - summaryInnerReturnCompare5, - summaryInnerReturnCompare6 - ), - displayNames = listOf( - "b < 0 : True -> return a", - "b == 10 : True -> return c", - "a < b : False -> return c", - "a < b : True -> return a", - "a < 0 : False -> return a", - "a > b : True -> return b" - ) - ) - } -} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/controlflow/SummaryCycleTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/controlflow/SummaryCycleTest.kt deleted file mode 100644 index f5e655d102..0000000000 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/controlflow/SummaryCycleTest.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.utbot.analytics.examples.controlflow - -import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest -import org.utbot.examples.controlflow.Cycles -import org.junit.jupiter.api.Test - -class SummaryCycleTest : SummaryTestCaseGeneratorTest( - Cycles::class, -) { - - val summaryLoopInsideLoop1 = "
\n" +
-            "Test iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (i < 0): True}\n" +
-            "returns from: {@code return 2;}\n" +
-            "
" - val summaryLoopInsideLoop2 = "
\n" +
-            "Test does not iterate {@code for(int i = x - 5; i < x; i++)}, {@code for(int j = i; j < x + i; j++)}, returns from: {@code return -1;}\n" +
-            "
" - val summaryLoopInsideLoop3 = "
\n" +
-            "Test iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (i < 0): False}\n" +
-            "iterates the loop {@code for(int j = i; j < x + i; j++)} once,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "    {@code (j == 7): True}\n" +
-            "returns from: {@code return 1;}\n" +
-            "
" - val summaryLoopInsideLoop4 = "
\n" +
-            "Test iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (i < 0): False}\n" +
-            "iterates the loop {@code for(int j = i; j < x + i; j++)} twice,\n" +
-            "        inside this loop, the test executes conditions:\n" +
-            "    {@code (j == 7): False}\n" +
-            "    {@code (j == 7): True}\n" +
-            "returns from: {@code return 1;}\n" +
-            "
" - val summaryLoopInsideLoop5 = "
\n" +
-            "Test iterates the loop {@code for(int i = x - 5; i < x; i++)} 5 times."
-
-    @Test
-    fun testLoopInsideLoop() {
-        checkOneArgument(
-            Cycles::loopInsideLoop,
-            summaryKeys = listOf(
-                summaryLoopInsideLoop1,
-                summaryLoopInsideLoop2,
-                summaryLoopInsideLoop3,
-                summaryLoopInsideLoop4,
-                summaryLoopInsideLoop5
-            ),
-            displayNames = listOf(
-                "i < 0 : True -> return 2",
-                "-> return -1",
-                "i < 0 : False -> return 1",
-                "j == 7 : False -> return 1",
-                "-> return -1"
-            )
-        )
-    }
-
-    val summaryStructureLoop1 = "
\n" +
-            "Test does not iterate {@code for(int i = 0; i < x; i++)}, returns from: {@code return -1;}\n" +
-            "
" - val summaryStructureLoop2 = "
\n" +
-            "Test iterates the loop {@code for(int i = 0; i < x; i++)} once,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (i == 2): False}\n"
-    val summaryStructureLoop3 = "
\n" +
-            "Test iterates the loop {@code for(int i = 0; i < x; i++)} 3 times,\n" +
-            "    inside this loop, the test executes conditions:\n" +
-            "    {@code (i == 2): True}\n" +
-            "returns from: {@code return 1;}\n" +
-            "
" - - @Test - fun testStructureLoop() { - checkOneArgument( - Cycles::structureLoop, - summaryKeys = listOf( - summaryStructureLoop1, - summaryStructureLoop2, - summaryStructureLoop3 - ), - displayNames = listOf( - "-> return -1", - "i == 2 : False -> return -1", - "i == 2 : True -> return 1" - ) - ) - } - -} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryInnerCallsTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryInnerCallsTest.kt deleted file mode 100644 index 976f3d969e..0000000000 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryInnerCallsTest.kt +++ /dev/null @@ -1,409 +0,0 @@ -package org.utbot.analytics.examples.inner - -import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest -import org.utbot.examples.inner.InnerCalls -import org.junit.jupiter.api.Test - -class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( - InnerCalls::class, -) { - - val keyCallLoopInsideLoop1 = "Test calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): True}\n" + - " returns from: {@code return 2;}" - val keyCallLoopInsideLoop2 = "Test calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): False}\n" + - " iterates the loop {@code for(int j = i; j < x + i; j++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (j == 7): True}\n" + - " returns from: {@code return 1;}" - val keyCallLoopInsideLoop3 = "Test calls Cycles::loopInsideLoop,\n" + - " there it does not iterate {@code for(int i = x - 5; i < x; i++)}, {@code for(int j = i; j < x + i; j++)}, returns from: {@code return -1;}" - val keyCallLoopInsideLoop4 = "Test calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): False}\n" + - " iterates the loop {@code for(int j = i; j < x + i; j++)} twice,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (j == 7): False}\n" + - " {@code (j == 7): True}\n" + - " returns from: {@code return 1;}" - val keyCallLoopInsideLoop5 = "Test calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} 5 times. " - - @Test - fun testCallLoopInsideLoop() { - checkOneArgument( - InnerCalls::callLoopInsideLoop, - summaryKeys = listOf( - keyCallLoopInsideLoop1, - keyCallLoopInsideLoop2, - keyCallLoopInsideLoop3, - keyCallLoopInsideLoop4, - keyCallLoopInsideLoop5 - ), - displayNames = listOf( - "i < 0 : True -> return 2", - "i < 0 : False -> return 1", - "loopInsideLoop -> return -1", - "j == 7 : False -> return 1", - "loopInsideLoop -> return -1" - ) - ) - } - - val keyInnerCallLeftSearch1 = - "Test throws IllegalArgumentException in: return binarySearch.leftBinSearch(array, key);" - val keyInnerCallLeftSearch2 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it invokes:\n" + - " BinarySearch::isUnsorted once\n" + - " triggers recursion of leftBinSearch once.\n" + - "Test throws NullPointerException in: return binarySearch.leftBinSearch(array, key);" - - val keyInnerCallLeftSearch3 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it does not iterate {@code while(left < right - 1)}, executes conditions:\n" + - " {@code (found): False}" - val keyInnerCallLeftSearch4 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it executes conditions:\n" + - " {@code (isUnsorted(array)): True}\n" + - "Test throws NullPointerException in: return binarySearch.leftBinSearch(array, key);" - - val keyInnerCallLeftSearch5 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it iterates the loop {@code while(left < right - 1)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (array[middle] == key): False},\n" + - " {@code (array[middle] < key): True}" - val keyInnerCallLeftSearch6 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it iterates the loop {@code while(left < right - 1)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (array[middle] == key): False},\n" + - " {@code (array[middle] < key): False}" - val keyInnerCallLeftSearch7 = "Test calls BinarySearch::leftBinSearch,\n" + - " there it iterates the loop {@code while(left < right - 1)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (array[middle] == key): True},\n" + - " {@code (array[middle] < key): False}" - - - @Test - fun testCallLeftBinSearch() { - checkTwoArguments( - InnerCalls::callLeftBinSearch, - summaryKeys = listOf( - keyInnerCallLeftSearch1, - keyInnerCallLeftSearch2, - keyInnerCallLeftSearch3, - keyInnerCallLeftSearch4, - keyInnerCallLeftSearch5, - keyInnerCallLeftSearch6, - keyInnerCallLeftSearch7 - ) - ) - } - - val keySummaryThreeDimensionalArray1 = "Test calls ArrayOfArrays::createNewThreeDimensionalArray,\n" + - " there it executes conditions:\n" + - " {@code (length != 2): True}\n" + - " returns from: {@code return new int[0][][];}" - val keySummaryThreeDimensionalArray2 = "Test calls ArrayOfArrays::createNewThreeDimensionalArray,\n" + - " there it executes conditions:\n" + - " {@code (length != 2): False}" - - // TODO: SAT-1211 - @Test - fun testCallCreateNewThreeDimensionalArray() { - checkTwoArguments( - InnerCalls::callCreateNewThreeDimensionalArray, - summaryKeys = listOf( - keySummaryThreeDimensionalArray1, - keySummaryThreeDimensionalArray2 - ), - displayNames = listOf( - "length != 2 : True -> return new int[0][][]", - "length != 2 : False -> return matrix" - ) - ) - } - - val summaryCallInitExample1 = "Test calls ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code NegativeArraySizeException e}\n" + - " returns from: {@code return -2;}" - val summaryCallInitExample2 = "Test calls ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code IndexOutOfBoundsException e}\n" + - " returns from: {@code return -3;}" - val summaryCallInitExample3 = "Test calls ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code IndexOutOfBoundsException e}\n" + - " returns from: {@code return -3;}" - val summaryCallInitExample4 = "Test calls ExceptionExamples::initAnArray,\n" + - " there it returns from: {@code return a[n - 1] + a[n - 2];}" - - @Test - fun testCallInitExamples() { - checkOneArgument( - InnerCalls::callInitExamples, - summaryKeys = listOf( - summaryCallInitExample1, - summaryCallInitExample2, - summaryCallInitExample3, - summaryCallInitExample4 - ) - ) - } - - val summaryCallFactorial1 = "Test calls Recursion::factorial,\n" + - " there it executes conditions:\n" + - " {@code (n == 0): True}\n" + - " returns from: {@code return 1;}" - val summaryCallFactorial2 = "Test calls Recursion::factorial,\n" + - " there it executes conditions:\n" + - " {@code (n < 0): True}\n" + - " triggers recursion of factorial once." - val summaryCallFactorial3 = "Test calls Recursion::factorial,\n" + - " there it executes conditions:\n" + - " {@code (n == 0): False}\n" + - " triggers recursion of factorial once, returns from: {@code return n * factorial(n - 1);}" - - @Test - fun testCallFactorial() { - checkOneArgument( - InnerCalls::callFactorial, - summaryKeys = listOf( - summaryCallFactorial1, - summaryCallFactorial2, - summaryCallFactorial3 - ), - displayNames = listOf( - "n == 0 : True -> return 1", - "n == 0 : False -> return n * factorial(n - 1)", - "return r.factorial(n) : True -> ThrowIllegalArgumentException" - ) - ) - } - - val summaryCallSimpleInvoke1 = "Test calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): True}" - val summaryCallSimpleInvoke2 = "Test calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): False},\n" + - " {@code (snd < 100): True}" - val summaryCallSimpleInvoke3 = "Test calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): False},\n" + - " {@code (snd < 100): False}\n" + - " invokes:\n" + - " InvokeExample::half once,\n" + - " InvokeExample::mult once" - - @Test - fun testCallSimpleInvoke() { - checkTwoArguments( - InnerCalls::callSimpleInvoke, - summaryKeys = listOf( - summaryCallSimpleInvoke1, - summaryCallSimpleInvoke2, - summaryCallSimpleInvoke3 - ) - ) - } - - val summaryCallStringExample1 = "Test calls StringExamples::indexOf,\n" + - " there it invokes:\n" + - " String::indexOf once" - val summaryCallStringExample2 = "Test calls StringExamples::indexOf,\n" + - " there it invokes:\n" + - " String::indexOf once" - val summaryCallStringExample3 = "Test calls StringExamples::indexOf,\n" + - " there it invokes:\n" + - " String::indexOf once" - - @Test - fun testCallComplicatedMethod() { - checkTwoArguments( - InnerCalls::callStringExample, - summaryKeys = listOf( - summaryCallStringExample1, - summaryCallStringExample2, - summaryCallStringExample3 - ) - ) - } - - val summarySimpleSwitchCase1 = "Test calls Switch::simpleSwitch,\n" + - " there it activates switch case:" - val summarySimpleSwitchCase2 = "Test calls Switch::simpleSwitch,\n" + - " there it activates switch case: {@code 11}" - val summarySimpleSwitchCase3 = "Test calls Switch::simpleSwitch,\n" + - " there it activates switch case: {@code 12}" - val summarySimpleSwitchCase4 = "Test calls Switch::simpleSwitch,\n" + - " there it activates switch case: {@code 10}" - val summarySimpleSwitchCase5 = "Test calls Switch::simpleSwitch,\n" + - " there it activates switch case: {@code default}" - - @Test - fun testCallSimpleSwitch() { - checkOneArgument( - InnerCalls::callSimpleSwitch, - summaryKeys = listOf( - summarySimpleSwitchCase1, - summarySimpleSwitchCase2, - summarySimpleSwitchCase3, - summarySimpleSwitchCase4, - summarySimpleSwitchCase5 - ), - displayNames = listOf( - "switch(x) case: 13 -> return 13", - "switch(x) case: 11 -> return 12", - "switch(x) case: 12 -> return 12", - "switch(x) case: 10 -> return 10", - "switch(x) case: Default -> return -1" - ) - ) - } - - val summaryCallLookup1 = "Test calls Switch::lookupSwitch,\n" + - " there it activates switch case: {@code 30}" - val summaryCallLookup2 = "Test calls Switch::lookupSwitch,\n" + - " there it activates switch case: {@code 10}" - val summaryCallLookup3 = "Test calls Switch::lookupSwitch,\n" + - " there it activates switch case: {@code 20}" - val summaryCallLookup4 = "Test calls Switch::lookupSwitch,\n" + - " there it activates switch case: {@code 0}" - val summaryCallLookup5 = "Test calls Switch::lookupSwitch,\n" + - " there it activates switch case: {@code default}" - - @Test - fun testCallLookup() { - checkOneArgument( - InnerCalls::callLookup, - summaryKeys = listOf( - summaryCallLookup1, - summaryCallLookup2, - summaryCallLookup3, - summaryCallLookup4, - summaryCallLookup5 - ) - ) - } - - val summaryDoubleSimpleInvoke1 = "Test calls InnerCalls::callSimpleInvoke,\n" + - " there it calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): True}" - val summaryDoubleSimpleInvoke2 = "Test calls InnerCalls::callSimpleInvoke,\n" + - " there it calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): False},\n" + - " {@code (snd < 100): True}" - val summaryDoubleSimpleInvoke3 = "Test calls InnerCalls::callSimpleInvoke,\n" + - " there it calls InvokeExample::simpleFormula,\n" + - " there it executes conditions:\n" + - " {@code (fst < 100): False},\n" + - " {@code (snd < 100): False}\n" + - " invokes:\n" + - " InvokeExample::half once,\n" + - " InvokeExample::mult once\n" + - " returns from: {@code return mult(x, y);}" - - @Test - fun testDoubleCall() { - checkTwoArguments( - InnerCalls::doubleSimpleInvoke, - summaryKeys = listOf( - summaryDoubleSimpleInvoke1, - summaryDoubleSimpleInvoke2, - summaryDoubleSimpleInvoke3 - ) - ) - } - - val summaryDoubleCallLoopInsideLoop1 = "Test calls InnerCalls::callLoopInsideLoop,\n" + - " there it calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): True}\n" + - " returns from: {@code return 2;}" - val summaryDoubleCallLoopInsideLoop2 = "Test calls InnerCalls::callLoopInsideLoop,\n" + - " there it calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): False}\n" + - " iterates the loop {@code for(int j = i; j < x + i; j++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (j == 7): True}\n" + - " returns from: {@code return 1;}" - val summaryDoubleCallLoopInsideLoop3 = "Test calls InnerCalls::callLoopInsideLoop,\n" + - " there it calls Cycles::loopInsideLoop,\n" + - " there it does not iterate {@code for(int i = x - 5; i < x; i++)}, {@code for(int j = i; j < x + i; j++)}, returns from: {@code return -1;}" - val summaryDoubleCallLoopInsideLoop4 = "Test calls InnerCalls::callLoopInsideLoop,\n" + - " there it calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} once,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (i < 0): False}\n" + - " iterates the loop {@code for(int j = i; j < x + i; j++)} 3 times,\n" + - " inside this loop, the test executes conditions:\n" + - " {@code (j == 7): False}\n" + - " {@code (j == 7): True}\n" + - " returns from: {@code return 1;}" - val summaryDoubleCallLoopInsideLoop5 = "Test calls InnerCalls::callLoopInsideLoop,\n" + - " there it calls Cycles::loopInsideLoop,\n" + - " there it iterates the loop {@code for(int i = x - 5; i < x; i++)} 5 times." - - - @Test - fun testDoubleCallLoopInsideLoop() { - checkOneArgument( - InnerCalls::doubleCallLoopInsideLoop, - summaryKeys = listOf( - summaryDoubleCallLoopInsideLoop1, - summaryDoubleCallLoopInsideLoop2, - summaryDoubleCallLoopInsideLoop3, - summaryDoubleCallLoopInsideLoop4, - summaryDoubleCallLoopInsideLoop5, - ) - ) - } - - val summaryCallFib1 = "Test calls Recursion::fib,\n" + - " there it executes conditions:\n" + - " {@code (n == 0): True}" - val summaryCallFib2 = "Test calls Recursion::fib,\n" + - " there it executes conditions:\n" + - " {@code (n == 0): False},\n" + - " {@code (n == 1): True}" - val summaryCallFib3 = "Test calls Recursion::fib,\n" + - " there it executes conditions:\n" + - " {@code (n == 0): False},\n" + - " {@code (n == 1): False}\n" + - " triggers recursion of fib twice, returns from: {@code return fib(n - 1) + fib(n - 2);}" - val summaryCallFib4 = "Test calls Recursion::fib,\n" + - " there it executes conditions:\n" + - " {@code (n < 0): True}\n" + - " triggers recursion of fib once." - - @Test - fun testInnerCallFib() { - checkOneArgument( - InnerCalls::callFib, - summaryKeys = listOf( - summaryCallFib1, - summaryCallFib2, - summaryCallFib3, - summaryCallFib4 - ), - displayNames = listOf( - "n == 0 : True -> return 0", - "n == 1 : True -> return 1", - "n == 1 : False -> return fib(n - 1) + fib(n - 2)", - "return r.fib(n) : True -> ThrowIllegalArgumentException" - ) - ) - } -} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryNestedCallsTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryNestedCallsTest.kt deleted file mode 100644 index 3e831c2e9e..0000000000 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/inner/SummaryNestedCallsTest.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.utbot.analytics.examples.inner - -import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest -import org.utbot.examples.inner.NestedCalls -import org.junit.jupiter.api.Test - -class SummaryNestedCallsTest : SummaryTestCaseGeneratorTest( - NestedCalls::class, -) { - - val summaryNestedCall1 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code NegativeArraySizeException e}" - val summaryNestedCall2 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code IndexOutOfBoundsException e}" - val summaryNestedCall3 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + - " there it catches exception:\n" + - " {@code IndexOutOfBoundsException e}" - val summaryNestedCall4 = "
\n" +
-            "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" +
-            "    there it returns from: {@code return a[n - 1] + a[n - 2];}"
-
-    @Test
-    fun testInvokeExample() {
-        checkOneArgument(
-            NestedCalls::callInitExamples,
-            summaryKeys = listOf(
-                summaryNestedCall1,
-                summaryNestedCall2,
-                summaryNestedCall3,
-                summaryNestedCall4,
-            ),
-            displayNames = listOf(
-                "Catch (NegativeArraySizeException e) -> return -2",
-                "Catch (IndexOutOfBoundsException e) -> return -3",
-                "Catch (IndexOutOfBoundsException e) -> return -3",
-                "initAnArray -> return a[n - 1] + a[n - 2]"
-            )
-        )
-    }
-}
\ No newline at end of file
diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/ternary/SummaryTernary.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/ternary/SummaryTernary.kt
deleted file mode 100644
index 8d3571feb1..0000000000
--- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/ternary/SummaryTernary.kt
+++ /dev/null
@@ -1,381 +0,0 @@
-package org.utbot.analytics.examples.ternary
-
-import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest
-import org.utbot.examples.ternary.Ternary
-import org.junit.jupiter.api.Tag
-import org.junit.jupiter.api.Test
-
-class SummaryTernary : SummaryTestCaseGeneratorTest(
-    Ternary::class,
-) {
-
-    val summaryMax1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (val1 >= val2): False}\n" +
-            "returns from: {@code return val1 >= val2 ? val1 : val2;}\n" +
-            "
" - val summaryMax2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (val1 >= val2): True}\n" +
-            "returns from: {@code return val1 >= val2 ? val1 : val2;}\n" +
-            "
" - - @Test - fun testMax() { - checkTwoArguments( - Ternary::max, - summaryKeys = listOf( - summaryMax1, - summaryMax2 - ), - displayNames = listOf( - "val1 >= val2 : False -> return val1 >= val2 ? val1 : val2", - "val1 >= val2 : True -> return val1 >= val2 ? val1 : val2" - ) - ) - } - - val summarySimpleOperation = "
\n" +
-            "Test returns from: {@code return result;}\n" +
-            "
" - - @Test - fun testSimpleOperation() { - checkTwoArguments( - Ternary::simpleOperation, - summaryKeys = listOf(summarySimpleOperation), - displayNames = listOf( - "-> return result" - ) - ) - } - - val summaryStringExpr1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num > 10): True}\n" +
-            "returns from: {@code return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";}\n" +
-            "
" - val summaryStringExpr2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num > 10): False},\n" +
-            "    {@code (num > 5): False}\n" +
-            "returns from: {@code return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";}\n" +
-            "
" - val summaryStringExpr3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num > 10): False},\n" +
-            "    {@code (num > 5): True}\n" +
-            "returns from: {@code return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";}\n" +
-            "
" - - @Test - fun testStringExpr() { - checkOneArgument( - Ternary::stringExpr, - summaryKeys = listOf( - summaryStringExpr1, - summaryStringExpr2, - summaryStringExpr3 - ) - ) - } - - @Test - @Tag("slow") - fun testParse() { - checkOneArgument( - Ternary::parse, - summaryKeys = listOf() - ) - } - - val summaryMinValue1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code ((a < b)): False}\n" +
-            "returns from: {@code return (a < b) ? a : b;}\n" +
-            "
" - val summaryMinValue2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code ((a < b)): True}\n" +
-            "returns from: {@code return (a < b) ? a : b;}\n" +
-            "
" - - @Test - fun testMinValue() { - checkTwoArguments( - Ternary::minValue, - summaryKeys = listOf( - summaryMinValue1, - summaryMinValue2 - ), - displayNames = listOf( - "a < b : False -> return (a < b) ? a : b", - "a < b : True -> return (a < b) ? a : b" - ) - ) - } - - val summarySubDelay1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (flag): False}\n" +
-            "returns from: {@code return flag ? 100 : 0;}\n" +
-            "
" - val summarySubDelay2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (flag): True}\n" +
-            "returns from: {@code return flag ? 100 : 0;}\n" +
-            "
" - - @Test - fun testSubDelay() { - checkOneArgument( - Ternary::subDelay, - summaryKeys = listOf( - summarySubDelay1, - summarySubDelay2 - ), - displayNames = listOf( - "flag : False -> return flag ? 100 : 0", - "flag : True -> return flag ? 100 : 0" - ) - ) - } - - val summaryPlusOrMinus1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code ((num1 > num2)): False}\n" +
-            "returns from: {@code return (num1 > num2) ? (num1 + num2) : (num1 - num2);}\n" +
-            "
" - val summaryPlusOrMinus2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code ((num1 > num2)): True}\n" +
-            "returns from: {@code return (num1 > num2) ? (num1 + num2) : (num1 - num2);}\n" +
-            "
" - - @Test - fun testPlusOrMinus() { - checkTwoArguments( - Ternary::plusOrMinus, - summaryKeys = listOf( - summaryPlusOrMinus1, - summaryPlusOrMinus2 - ) - ) - } - - val summaryLongTernary1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;}\n" +
-            "
" - val summaryLongTernary2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False},\n" +
-            "    {@code (num1 == num2): True}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;}\n" +
-            "
" - val summaryLongTernary3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False},\n" +
-            "    {@code (num1 == num2): False}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;}\n" +
-            "
" - - @Test - fun testLongTernary() { - checkTwoArguments( - Ternary::longTernary, - summaryKeys = listOf( - summaryLongTernary1, - summaryLongTernary2, - summaryLongTernary3 - ) - ) - } - - val summaryVeryLongTernary1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;}\n" +
-            "
" - val summaryVeryLongTernary2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False},\n" +
-            "    {@code (num1 == num2): False},\n" +
-            "    {@code (num2 > num3): False},\n" +
-            "    {@code (num2 == num3): False}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;}\n" +
-            "
" - val summaryVeryLongTernary3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False},\n" +
-            "    {@code (num1 == num2): True}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;}\n" +
-            "
" - val summaryVeryLongTernary4 = "Test executes conditions:\n" + - " {@code (num1 > num2): False},\n" + - " {@code (num1 == num2): False},\n" + - " {@code (num2 > num3): False},\n" + - " {@code (num2 == num3): True}\n" + - "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;}\n" + - "
" - val summaryVeryLongTernary5 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False},\n" +
-            "    {@code (num1 == num2): False},\n" +
-            "    {@code (num2 > num3): True}\n" +
-            "returns from: {@code return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;}\n" +
-            "
" - - @Test - fun testVeryLongTernary() { - checkThreeArguments( - Ternary::veryLongTernary, - summaryKeys = listOf( - summaryVeryLongTernary1, - summaryVeryLongTernary2, - summaryVeryLongTernary3, - summaryVeryLongTernary4, - summaryVeryLongTernary5 - ) - ) - } - - val summaryMinMax1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False}\n" +
-            "calls Ternary::minValue,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code ((a < b)): True}\n" +
-            "    returns from: {@code return (a < b) ? a : b;}"
-    val summaryMinMax2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True}\n" +
-            "calls Ternary::max,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (val1 >= val2): True}\n" +
-            "    returns from: {@code return val1 >= val2 ? val1 : val2;}"
-    val summaryMinMax3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False}\n" +
-            "calls Ternary::minValue,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code ((a < b)): False}\n" +
-            "    returns from: {@code return (a < b) ? a : b;}"
-
-    @Test
-    fun testMinMax() {
-        checkTwoArguments(
-            Ternary::minMax,
-            summaryKeys = listOf(
-                summaryMinMax1,
-                summaryMinMax2,
-                summaryMinMax3
-            )
-        )
-    }
-
-    val summaryIncFunc1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False}\n" +
-            "invokes:\n" +
-            "    Ternary::intFunc2 once\n" +
-            "returns from: {@code return num1 > num2 ? intFunc1() : intFunc2();}\n" +
-            "
" - val summaryIncFunc2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True}\n" +
-            "invokes:\n" +
-            "    Ternary::intFunc1 once\n" +
-            "returns from: {@code return num1 > num2 ? intFunc1() : intFunc2();}\n" +
-            "
" - - @Test - fun testIntFunc() { - checkTwoArguments( - Ternary::intFunc, - summaryKeys = listOf( - summaryIncFunc1, - summaryIncFunc2 - ), - displayNames = listOf( - "num1 > num2 : False -> return num1 > num2 ? intFunc1() : intFunc2()", - "num1 > num2 : True -> return num1 > num2 ? intFunc1() : intFunc2()" - ) - ) - } - - val summaryTernaryInTheMiddle1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num2 > num3): True}\n" +
-            "calls Ternary::max,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (val1 >= val2): False}\n" +
-            "    returns from: {@code return val1 >= val2 ? val1 : val2;}"
-    val summaryTernaryInTheMiddle2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num2 > num3): False}\n" +
-            "calls Ternary::max,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (val1 >= val2): False}\n" +
-            "    returns from: {@code return val1 >= val2 ? val1 : val2;}"
-    val summaryTernaryInTheMiddle3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num2 > num3): False}\n" +
-            "calls Ternary::max,\n" +
-            "    there it executes conditions:\n" +
-            "        {@code (val1 >= val2): True}\n" +
-            "    returns from: {@code return val1 >= val2 ? val1 : val2;}"
-
-    @Test
-    fun testTernaryInTheMiddle() {
-        checkThreeArguments(
-            Ternary::ternaryInTheMiddle,
-            summaryKeys = listOf(
-                summaryTernaryInTheMiddle1,
-                summaryTernaryInTheMiddle2,
-                summaryTernaryInTheMiddle3
-            ),
-            displayNames = listOf(
-                "val1 >= val2 : False -> return val1 >= val2 ? val1 : val2",
-                "val2 : False -> return val1 >= val2 ? val1 : val2",
-                "val1 >= val2 : True -> return val1 >= val2 ? val1 : val2"
-            )
-        )
-    }
-
-    val summaryTwoIfsOneLine1 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): False}\n" +
-            "returns from: {@code return a;}\n" +
-            "
" - val summaryTwoIfsOneLine2 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True},\n" +
-            "    {@code ((num1 - 10) > 0): False}\n" +
-            "returns from: {@code return a;}\n" +
-            "
" - val summaryTwoIfsOneLine3 = "
\n" +
-            "Test executes conditions:\n" +
-            "    {@code (num1 > num2): True},\n" +
-            "    {@code ((num1 - 10) > 0): True}\n" +
-            "returns from: {@code return a;}\n" +
-            "
" - - @Test - fun testTwoIfsOneLine() { - checkTwoArguments( - Ternary::twoIfsOneLine, - summaryKeys = listOf( - summaryTwoIfsOneLine1, - summaryTwoIfsOneLine2, - summaryTwoIfsOneLine3 - ), - displayNames = listOf( - "num1 > num2 : False -> return a", - "(num1 - 10) > 0 : False -> return a", - "(num1 - 10) > 0 : True -> return a" - ) - ) - } -} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/guava/math/SummaryIntMath.kt b/utbot-analytics/src/test/kotlin/org/utbot/analytics/guava/math/SummaryIntMath.kt deleted file mode 100644 index d92c84f9d3..0000000000 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/guava/math/SummaryIntMath.kt +++ /dev/null @@ -1,112 +0,0 @@ -package org.utbot.analytics.guava.math - -import org.utbot.analytics.examples.SummaryTestCaseGeneratorTest -import guava.examples.math.IntMath -import org.junit.jupiter.api.Test - -class SummaryIntMath : SummaryTestCaseGeneratorTest( - IntMath::class, -) { - - //TODO SAT-1205 - @Test - fun testLog2() { - checkOneArgument( - IntMath::log2, - summaryKeys = listOf() - ) - } - - @Test - fun testPow() { - val summaryPow1 = "
\n" +
-                "Test activates switch case: {@code 2}, returns from: {@code return 1;}\n" +
-                "
" - val summaryPow2 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code (k < Integer.SIZE): False}\n" +
-                "returns from: {@code return 0;}\n" +
-                "
" - val summaryPow3 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code ((k < Integer.SIZE)): False}\n" +
-                "returns from: {@code return (k < Integer.SIZE) ? (1 << k) : 0;}\n" +
-                "
" - val summaryPow4 = "
\n" +
-                "Test iterates the loop {@code for(int accum = 1; ; k >>= 1)} once,\n" +
-                "    inside this loop, the test returns from: {@code return b * accum;}\n" +
-                "
" - val summaryPow5 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code ((k < Integer.SIZE)): True}\n" +
-                "returns from: {@code return (k < Integer.SIZE) ? (1 << k) : 0;}\n" +
-                "
" - val summaryPow6 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code ((k == 0)): False}\n" +
-                "returns from: {@code return (k == 0) ? 1 : 0;}\n" +
-                "
" - val summaryPow7 = "
\n" +
-                "Test iterates the loop {@code for(int accum = 1; ; k >>= 1)} once,\n" +
-                "    inside this loop, the test returns from: {@code return accum;}\n" +
-                "
" - val summaryPow8 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code ((k == 0)): True}\n" +
-                "returns from: {@code return (k == 0) ? 1 : 0;}\n" +
-                "
" - val summaryPow9 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code (k < Integer.SIZE): True},\n" +
-                "    {@code (((k & 1) == 0)): True}\n" +
-                "returns from: {@code return ((k & 1) == 0) ? (1 << k) : -(1 << k);}\n" +
-                "
" - val summaryPow10 = "
\n" +
-                "Test iterates the loop {@code for(int accum = 1; ; k >>= 1)} twice,\n" +
-                "    inside this loop, the test executes conditions:\n" +
-                "    {@code (((k & 1) == 0)): False}\n" +
-                ", returns from: {@code return b * accum;}\n" +
-                "
" - val summaryPow11 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code (((k & 1) == 0)): False}\n" +
-                "returns from: {@code return ((k & 1) == 0) ? 1 : -1;}\n" +
-                "
" - val summaryPow12 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code (k < Integer.SIZE): True},\n" +
-                "    {@code (((k & 1) == 0)): False}\n" +
-                "returns from: {@code return ((k & 1) == 0) ? (1 << k) : -(1 << k);}\n" +
-                "
" - val summaryPow13 = "
\n" +
-                "Test executes conditions:\n" +
-                "    {@code (((k & 1) == 0)): True}\n" +
-                "returns from: {@code return ((k & 1) == 0) ? 1 : -1;}\n" +
-                "
" - val summaryPow14 = "
\n" +
-                "Test iterates the loop {@code for(int accum = 1; ; k >>= 1)} twice,\n" +
-                "    inside this loop, the test executes conditions:\n" +
-                "    {@code (((k & 1) == 0)): True}\n" +
-                ", returns from: {@code return b * accum;}\n" +
-                "
" - checkOneArgument( - IntMath::pow, - summaryKeys = listOf( - summaryPow1, - summaryPow2, - summaryPow3, - summaryPow4, - summaryPow5, - summaryPow6, - summaryPow7, - summaryPow8, - summaryPow9, - summaryPow10, - summaryPow11, - summaryPow12, - summaryPow13, - summaryPow14 - ) - ) - } -} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt new file mode 100644 index 0000000000..3c0176fd3e --- /dev/null +++ b/utbot-analytics/src/test/kotlin/org/utbot/features/FeatureProcessorWithRepetitionTest.kt @@ -0,0 +1,111 @@ +package org.utbot.features + +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.utbot.analytics.EngineAnalyticsContext +import org.utbot.examples.AbstractTestCaseGeneratorTest +import org.utbot.examples.eq +import org.utbot.examples.withFeaturePath +import java.io.File +import java.io.FileInputStream + +class FeatureProcessorWithRepetitionTest : AbstractTestCaseGeneratorTest(OnePath::class, false) { + companion object { + const val featureDir = "src/test/resources/features" + fun reward(coverage: Double, time: Double) = RewardEstimator.reward(coverage, time) + + @JvmStatic + @BeforeAll + fun beforeAll() { + File(featureDir).mkdir() + EngineAnalyticsContext.featureProcessorFactory = FeatureProcessorWithStatesRepetitionFactory() + EngineAnalyticsContext.featureExtractorFactory = FeatureExtractorFactoryImpl() + } + + @JvmStatic + @AfterAll + fun afterAll() { + File(featureDir).deleteRecursively() + } + } + + @Test + fun testCalculateRewards() { + val statesToInt = mapOf( + "a0" to 1, + "c0" to 2, + "f0" to 3, + "g0" to 4, + "c1" to 5, + "f1" to 6, + "g1" to 7, + "b0" to 8, + "d0" to 9 + ) + + val expectedRewards: Map = mapOf( + "a0" to reward(6.0, 15.0), + "c0" to reward(4.0, 10.0), + "f0" to reward(4.0, 8.0), + "g0" to reward(4.0, 2.0), + "c1" to reward(0.0, 4.0), + "f1" to reward(0.0, 3.0), + "g1" to reward(0.0, 2.0), + "b0" to reward(2.0, 4.0), + "d0" to reward(2.0, 2.0) + ) + + val rewardEstimator = RewardEstimator() + val testCases = listOf( + TestCase( + listOf( + statesToInt["g0"]!! to 2L, + statesToInt["f0"]!! to 2L, + statesToInt["c0"]!! to 2L, + statesToInt["a0"]!! to 1L + ), + newCoverage = 4, testIndex = 0 + ), + TestCase( + listOf( + statesToInt["g1"]!! to 2L, + statesToInt["f1"]!! to 1L, + statesToInt["c1"]!! to 1L, + statesToInt["f0"]!! to 2L, + statesToInt["c0"]!! to 2L, + statesToInt["a0"]!! to 1L + ), + newCoverage = 0, + testIndex = 1 + ), + TestCase( + listOf(statesToInt["d0"]!! to 2L, statesToInt["b0"]!! to 2L, statesToInt["a0"]!! to 1L), + newCoverage = 2, + testIndex = 2 + ) + ) + + val rewards = rewardEstimator.calculateRewards(testCases) + Assertions.assertEquals(9, rewards.size) + expectedRewards.forEach { + Assertions.assertEquals(it.value, rewards[statesToInt[it.key]]) + } + } + + /** + * Test, that we correctly add test cases and dump them into file + */ + @Test + fun addTestCaseTest() { + withFeaturePath(featureDir) { + check( + OnePath::onePath, + eq(1) + ) + + Assertions.assertTrue(FileInputStream("$featureDir/0.csv").bufferedReader().readLines().size > 1) + } + } +} diff --git a/utbot-analytics/src/test/kotlin/org/utbot/predictors/LinearStateRewardPredictorTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/predictors/LinearStateRewardPredictorTest.kt new file mode 100644 index 0000000000..6a68e83212 --- /dev/null +++ b/utbot-analytics/src/test/kotlin/org/utbot/predictors/LinearStateRewardPredictorTest.kt @@ -0,0 +1,45 @@ +package org.utbot.predictors + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.utbot.examples.withPathSelectorType +import org.utbot.examples.withRewardModelPath +import org.utbot.framework.PathSelectorType +import org.utbot.framework.UtSettings + +class LinearStateRewardPredictorTest { + @Test + fun simpleTest() { + withRewardModelPath("src/test/resources") { + val pred = LinearStateRewardPredictor() + + val features = listOf( + listOf(2.0, 3.0), + listOf(2.0, 3.0) + ) + + assertEquals(listOf(6.0, 6.0), pred.predict(features)) + } + } + + @Test + fun wrongFormatTest() { + withRewardModelPath("src/test/resources") { + withPathSelectorType(PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + LinearStateRewardPredictor("wrong_format_linear.txt") + assertEquals(PathSelectorType.INHERITORS_SELECTOR, UtSettings.pathSelectorType) + } + } + } + + @Test + fun simpleTestNotBatch() { + withRewardModelPath("src/test/resources") { + val pred = LinearStateRewardPredictor() + + val features = listOf(2.0, 3.0) + + assertEquals(6.0, pred.predict(features)) + } + } +} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/predictors/NNStateRewardPredictorTest.kt b/utbot-analytics/src/test/kotlin/org/utbot/predictors/NNStateRewardPredictorTest.kt new file mode 100644 index 0000000000..42dd3bac40 --- /dev/null +++ b/utbot-analytics/src/test/kotlin/org/utbot/predictors/NNStateRewardPredictorTest.kt @@ -0,0 +1,86 @@ +package org.utbot.predictors + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.examples.withPathSelectorType +import org.utbot.analytics.StateRewardPredictor +import org.utbot.examples.withRewardModelPath +import org.utbot.framework.PathSelectorType +import org.utbot.framework.UtSettings +import kotlin.system.measureNanoTime + +class NNStateRewardPredictorTest { + @Test + fun simpleTest() { + withRewardModelPath("src/test/resources") { + val pred = NNStateRewardPredictorBase() + + val features = listOf(0.0, 0.0) + + assertEquals(5.0, pred.predict(features)) + } + } + + @Disabled("Just to see the performance of predictors") + @Test + fun performanceTest() { + val features = (1..13).map { 1.0 }.toList() + withRewardModelPath("models\\test\\0") { + val averageTime = calcAverageTimeForModelPredict(::NNStateRewardPredictorBase, 100, features) + println(averageTime) + } + + + withRewardModelPath("models") { + val averageTime = calcAverageTimeForModelPredict(::StateRewardPredictorTorch, 100, features) + println(averageTime) + } + } + + private fun calcAverageTimeForModelPredict( + model: () -> StateRewardPredictor, + iterations: Int, + features: List + ): Double { + val pred = model() + + (1..iterations).map { + pred.predict(features) + } + + return (1..iterations) + .map { measureNanoTime { pred.predict(features) } } + .average() + } + + @Test + fun corruptedModelFileTest() { + withRewardModelPath("src/test/resources") { + withPathSelectorType(PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + NNStateRewardPredictorBase(modelPath = "corrupted_nn.json") + assertEquals(PathSelectorType.INHERITORS_SELECTOR, UtSettings.pathSelectorType) + } + } + } + + @Test + fun emptyModelFileTest() { + withRewardModelPath("src/test/resources") { + withPathSelectorType(PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + NNStateRewardPredictorBase(modelPath = "empty_nn.json") + assertEquals(PathSelectorType.INHERITORS_SELECTOR, UtSettings.pathSelectorType) + } + } + } + + @Test + fun corruptedScalerTest() { + withRewardModelPath("src/test/resources") { + withPathSelectorType(PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + NNStateRewardPredictorBase(scalerPath = "corrupted_scaler.txt") + assertEquals(PathSelectorType.INHERITORS_SELECTOR, UtSettings.pathSelectorType) + } + } + } +} \ No newline at end of file diff --git a/utbot-analytics/src/test/resources/corrupted_nn.json b/utbot-analytics/src/test/resources/corrupted_nn.json new file mode 100644 index 0000000000..ffc3ef7976 --- /dev/null +++ b/utbot-analytics/src/test/resources/corrupted_nn.json @@ -0,0 +1,25 @@ +{ + dsfds + "linearLayers": [ + [ + [1, 0], + [0, 1] + ], + [ + [1, 0], + [0, 1] + ], + [ + [1, 1] + ] + ], + "activationLayers": [ + "reLU", + "reLU" + ], + "biases": [ + [1, 1], + [1, 1], + [1] + ] +} \ No newline at end of file diff --git a/utbot-analytics/src/test/resources/corrupted_scaler.txt b/utbot-analytics/src/test/resources/corrupted_scaler.txt new file mode 100644 index 0000000000..42ff95f905 --- /dev/null +++ b/utbot-analytics/src/test/resources/corrupted_scaler.txt @@ -0,0 +1 @@ +1,asada \ No newline at end of file diff --git a/utbot-analytics/src/test/resources/empty_nn.json b/utbot-analytics/src/test/resources/empty_nn.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/utbot-analytics/src/test/resources/linear.txt b/utbot-analytics/src/test/resources/linear.txt new file mode 100644 index 0000000000..8a6627b46a --- /dev/null +++ b/utbot-analytics/src/test/resources/linear.txt @@ -0,0 +1 @@ +1,1,1 \ No newline at end of file diff --git a/utbot-analytics/src/test/resources/nn.json b/utbot-analytics/src/test/resources/nn.json new file mode 100644 index 0000000000..945b53aef8 --- /dev/null +++ b/utbot-analytics/src/test/resources/nn.json @@ -0,0 +1,24 @@ +{ + "linearLayers": [ + [ + [1, 0], + [0, 1] + ], + [ + [1, 0], + [0, 1] + ], + [ + [1, 1] + ] + ], + "activationLayers": [ + "reLU", + "reLU" + ], + "biases": [ + [1, 1], + [1, 1], + [1] + ] +} \ No newline at end of file diff --git a/utbot-analytics/src/test/resources/scaler.txt b/utbot-analytics/src/test/resources/scaler.txt new file mode 100644 index 0000000000..b241d25f33 --- /dev/null +++ b/utbot-analytics/src/test/resources/scaler.txt @@ -0,0 +1,2 @@ +0,0 +1,1 diff --git a/utbot-analytics/src/test/resources/wrong_format_linear.txt b/utbot-analytics/src/test/resources/wrong_format_linear.txt new file mode 100644 index 0000000000..bf4d5a1b19 --- /dev/null +++ b/utbot-analytics/src/test/resources/wrong_format_linear.txt @@ -0,0 +1 @@ +1,asdasdsa,asdasd \ No newline at end of file diff --git a/utbot-api/build.gradle b/utbot-api/build.gradle index fc50f68057..e99e0d7078 100644 --- a/utbot-api/build.gradle +++ b/utbot-api/build.gradle @@ -1 +1,11 @@ +plugins { + id "com.github.johnrengelman.shadow" version "6.1.0" +} + apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" + +shadowJar { + configurations = [project.configurations.compileClasspath] + archiveClassifier.set('') + minimize() +} \ No newline at end of file diff --git a/utbot-api/src/main/java/org/utbot/api/mock/UtMock.java b/utbot-api/src/main/java/org/utbot/api/mock/UtMock.java index 1b7b512dfb..c7f7b2215b 100644 --- a/utbot-api/src/main/java/org/utbot/api/mock/UtMock.java +++ b/utbot-api/src/main/java/org/utbot/api/mock/UtMock.java @@ -13,6 +13,14 @@ public static T makeSymbolic(boolean isNullable) { @SuppressWarnings("unused") public static void assume(boolean predicate) { // to use compilers checks, i.e. for possible NPE - if (!predicate) throw new RuntimeException(); + if (!predicate) { + throw new RuntimeException(); + } + } + + @SuppressWarnings("unused") + public static void assumeOrExecuteConcretely(boolean predicate) { + // In oppose to assume, we don't have predicate check here + // to avoid RuntimeException during concrete execution } } \ No newline at end of file diff --git a/utbot-cli/build.gradle b/utbot-cli/build.gradle index 0f9d0bec4d..9e2106f3e7 100644 --- a/utbot-cli/build.gradle +++ b/utbot-cli/build.gradle @@ -1,15 +1,17 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" +configurations { + fetchInstrumentationJar +} + compileKotlin { kotlinOptions { allWarningsAsErrors = false } } - dependencies { api project(":utbot-framework") - api project(":utbot-analytics") api project(':utbot-summary') implementation group: 'org.mockito', name: 'mockito-core', version: mockito_version @@ -25,6 +27,13 @@ dependencies { compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2_version compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j2_version implementation group: 'org.jacoco', name: 'org.jacoco.report', version: jacoco_version + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } } task createProperties(dependsOn: processResources) { @@ -58,3 +67,4 @@ jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE } + diff --git a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt index d54288ea25..0380efaa7b 100644 --- a/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt +++ b/utbot-cli/src/main/kotlin/org/utbot/cli/GenerateTestsCommand.kt @@ -94,6 +94,9 @@ class GenerateTestsCommand : val targetMethods = classUnderTest.targetMethods() initializeEngine(workingDirectory) + if (targetMethods.isEmpty()) { + throw Exception("Nothing to process. No methods were provided") + } // utContext is used in `generateTestCases`, `generateTest`, `generateReport` withUtContext(UtContext(targetMethods.first().clazz.java.classLoader)) { diff --git a/utbot-core/build.gradle b/utbot-core/build.gradle index f70ab1014f..271a7a5651 100644 --- a/utbot-core/build.gradle +++ b/utbot-core/build.gradle @@ -1,6 +1,16 @@ +plugins { + id "com.github.johnrengelman.shadow" version "6.1.0" +} + apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" dependencies { implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version implementation group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0' } + +shadowJar { + configurations = [project.configurations.compileClasspath] + archiveClassifier.set('') + minimize() +} \ No newline at end of file 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 77f007eda1..cab4e7ff33 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/FileUtil.kt @@ -75,12 +75,8 @@ object FileUtil { }.forEach { it.deleteRecursively() } } - private fun createTempDirectory(): Path { - val tempFolderPath = Paths.get(tempDirectoryPath + File.separator + utBotTempFolderPrefix) - - tempFolderPath.toFile().mkdirs() - - return createTempDirectory(tempFolderPath, "generated-") + fun createTempDirectory(prefix: String): Path { + return createTempDirectory(utBotTempDirectory, prefix) } /** @@ -88,7 +84,7 @@ object FileUtil { * It can be used for Soot analysis. */ fun isolateClassFiles(vararg classes: KClass<*>): File { - val tempDir = createTempDirectory().toFile() + val tempDir = createTempDirectory("generated-").toFile() for (clazz in classes) { val path = clazz.toClassFilePath() diff --git a/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt b/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt index 7c8572d0d5..1f6bb848a5 100644 --- a/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt +++ b/utbot-core/src/main/kotlin/org/utbot/common/KClassUtil.kt @@ -28,4 +28,4 @@ fun Method.invokeCatching(obj: Any?, args: List) = try { Result.success(invoke(obj, *args.toTypedArray())) } catch (e: InvocationTargetException) { Result.failure(e.targetException) -} \ No newline at end of file +} diff --git a/utbot-framework-api/build.gradle b/utbot-framework-api/build.gradle index 96a133dcad..d922375eef 100644 --- a/utbot-framework-api/build.gradle +++ b/utbot-framework-api/build.gradle @@ -1,5 +1,8 @@ -apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" +plugins { + id "com.github.johnrengelman.shadow" version "6.1.0" +} +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" dependencies { api project(':utbot-core') @@ -9,4 +12,10 @@ dependencies { // TODO do we really need apache commons? implementation group: 'org.apache.commons', name: 'commons-lang3', version: commons_lang_version implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version +} + +shadowJar { + configurations = [project.configurations.compileClasspath] + archiveClassifier.set('') + minimize() } \ No newline at end of file diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt index b97d534627..6eee9a2d39 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/UtSettings.kt @@ -1,14 +1,12 @@ package org.utbot.framework +import mu.KotlinLogging import org.utbot.common.PathUtil.toPath import java.io.FileInputStream -import java.io.FileNotFoundException import java.io.IOException import java.util.Properties -import kotlin.io.path.exists import kotlin.properties.PropertyDelegateProvider import kotlin.reflect.KProperty -import mu.KotlinLogging private val logger = KotlinLogging.logger {} @@ -31,7 +29,7 @@ internal class SettingDelegate(val initializer: () -> T) { /** * Default concrete execution timeout (in milliseconds). */ -const val DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS = 100L +const val DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS = 1000L object UtSettings { private val properties = Properties().also { props -> @@ -70,7 +68,8 @@ object UtSettings { private fun getIntProperty(defaultValue: Int) = getProperty(defaultValue, String::toInt) private fun getLongProperty(defaultValue: Long) = getProperty(defaultValue, String::toLong) private fun getStringProperty(defaultValue: String) = getProperty(defaultValue) { it } - private inline fun > getEnumProperty(defaultValue: T) = getProperty(defaultValue) { enumValueOf(it) } + private inline fun > getEnumProperty(defaultValue: T) = + getProperty(defaultValue) { enumValueOf(it) } /** @@ -110,6 +109,16 @@ object UtSettings { */ var pathSelectorType: PathSelectorType by getEnumProperty(PathSelectorType.INHERITORS_SELECTOR) + /** + * Type of nnRewardGuidedSelector + */ + var nnRewardGuidedSelectorType: NNRewardGuidedSelectorType by getEnumProperty(NNRewardGuidedSelectorType.WITHOUT_RECALCULATION) + + /** + * Type of [StateRewardPredictor] + */ + var stateRewardPredictorType: StateRewardPredictorType by getEnumProperty(StateRewardPredictorType.BASE) + /** * Steps limit for path selector. */ @@ -154,7 +163,7 @@ object UtSettings { /* * Activate or deactivate tests on comments && names/displayNames * */ - var testSummary by getBooleanProperty( true) + var testSummary by getBooleanProperty(true) var testName by getBooleanProperty(true) var testDisplayName by getBooleanProperty(true) @@ -237,7 +246,17 @@ object UtSettings { /** * Set to true to start fuzzing if symbolic execution haven't return anything */ - var useFuzzing: Boolean by getBooleanProperty(false) + var useFuzzing: Boolean by getBooleanProperty(true) + + /** + * Set the total attempts to improve coverage by fuzzer. + */ + var fuzzingMaxAttempts: Int by getIntProperty(Int.MAX_VALUE) + + /** + * Fuzzer tries to generate and run tests during this time. + */ + var fuzzingTimeoutInMillis: Int by getIntProperty(3_000) /** * Generate tests that treat possible overflows in arithmetic operations as errors @@ -261,7 +280,17 @@ object UtSettings { /** * Timeout for specific concrete execution (in milliseconds). */ - var concreteExecutionTimeoutInChildProcess: Long by getLongProperty(DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS) + var concreteExecutionTimeoutInChildProcess: Long by getLongProperty( + DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS + ) + + /** + * Determines whether should errors from a child process be written to a log file or suppressed. + * Note: being enabled, this option can highly increase disk usage when using ContestEstimator. + * + * False by default (for saving disk space). + */ + var logConcreteExecutionErrors by getBooleanProperty(false) /** * Number of branch instructions using for clustering executions in the test minimization phase. @@ -283,6 +312,55 @@ object UtSettings { */ var enableUnsatCoreCalculationForHardConstraints by getBooleanProperty(false) + /** + * Enable it to process states with unknown solver status + * from the queue to concrete execution. + * + * True by default. + */ + var processUnknownStatesDuringConcreteExecution by getBooleanProperty(true) + + /** + * 2^{this} will be the length of observed subpath. + * See [SubpathGuidedSelector] + */ + var subpathGuidedSelectorIndex by getIntProperty(1) + + /** + * Set of indexes, which will use [SubpathGuidedSelector] in not single mode + */ + var subpathGuidedSelectorIndexes = listOf(0, 1, 2, 3) + + /** + * Flag that indicates whether feature processing for execution states enabled or not + */ + var enableFeatureProcess by getBooleanProperty(false) + + /** + * Path to deserialized reward models + */ + var rewardModelPath by getStringProperty("../models/0") + + /** + * Number of model iterations that will be used during ContestEstimator + */ + var iterations by getIntProperty(1) + + /** + * Path for state features dir + */ + var featurePath by getStringProperty("eval/secondFeatures/antlr/INHERITORS_SELECTOR") + + /** + * Counter for tests during testGeneration for one project in ContestEstimator + */ + var testCounter by getIntProperty(0) + + /** + * Flag for Subpath and NN selectors whether they are combined (Subpath use several indexes, NN use several models) + */ + var singleSelector by getBooleanProperty(true) + override fun toString(): String = properties .entries @@ -290,12 +368,87 @@ object UtSettings { .joinToString(separator = System.lineSeparator()) { "\t${it.key}=${it.value}" } } +/** + * Type of [BasePathSelector]. For each value see class in comment + */ enum class PathSelectorType { + /** + * [CoveredNewSelector] + */ COVERED_NEW_SELECTOR, - INHERITORS_SELECTOR + + /** + * [InheritorsSelector] + */ + INHERITORS_SELECTOR, + + /** + * [SubpathGuidedSelector] + */ + SUBPATH_GUIDED_SELECTOR, + + /** + * [CPInstSelector] + */ + CPI_SELECTOR, + + /** + * [ForkDepthSelector] + */ + FORK_DEPTH_SELECTOR, + + /** + * [NNRewardGuidedSelector] + */ + NN_REWARD_GUIDED_SELECTOR, + + /** + * [RandomSelector] + */ + RANDOM_SELECTOR, + + /** + * [RandomPathSelector] + */ + RANDOM_PATH_SELECTOR } enum class TestSelectionStrategyType { DO_NOT_MINIMIZE_STRATEGY, // Always adds new test COVERAGE_STRATEGY // Adds new test only if it increases coverage } + +/** + * Enum to specify [NNRewardGuidedSelector], see implementations for more details + */ +enum class NNRewardGuidedSelectorType { + /** + * [NNRewardGuidedSelectorWithRecalculation] + */ + WITH_RECALCULATION, + + /** + * [NNRewardGuidedSelectorWithoutRecalculation] + */ + WITHOUT_RECALCULATION +} + +/** + * Enum to specify [StateRewardPredictor], see implementations for details + */ +enum class StateRewardPredictorType { + /** + * [NNStateRewardPredictorBase] + */ + BASE, + + /** + * [StateRewardPredictorTorch] + */ + TORCH, + + /** + * [NNStateRewardPredictorBase] + */ + LINEAR +} 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 d67044b753..be42ca8e4b 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 @@ -420,7 +420,7 @@ data class UtArrayModel( override val id: Int, override val classId: ClassId, val length: Int = 0, - val constModel: UtModel, + var constModel: UtModel, val stores: MutableMap ) : UtReferenceModel(id, classId) { override fun toString() = withToStringThreadLocalReentrancyGuard { @@ -653,6 +653,9 @@ open class ClassId( open val packageName: String get() = jClass.`package`?.name ?: "" // empty package for primitives + open val isInDefaultPackage: Boolean + get() = packageName.isEmpty() + open val isPublic: Boolean get() = Modifier.isPublic(jClass.modifiers) @@ -1041,7 +1044,7 @@ enum class MockStrategyApi( override val displayName: String, override val description: String ) : CodeGenerationSettingItem { - NO_MOCKS("No mocks", "Do not use Mock frameworks at all"), + NO_MOCKS("No mocks", "Do not use mock frameworks at all"), OTHER_PACKAGES( "Other packages: $MOCKITO", "Mock all classes outside the current package except system ones" @@ -1102,7 +1105,7 @@ enum class MockFramework( enum class CodegenLanguage( override val displayName: String, - @Suppress("unused") override val description: String = "Generating unit tests in $displayName" + @Suppress("unused") override val description: String = "Generate unit tests in $displayName" ) : CodeGenerationSettingItem { JAVA(displayName = "Java"), KOTLIN(displayName = "Kotlin"); @@ -1155,6 +1158,9 @@ enum class CodegenLanguage( ).plus(sourcesFiles) KOTLIN -> listOf("-d", buildDirectory, "-jvm-target", jvmTarget, "-cp", classPath).plus(sourcesFiles) } + 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") + } return listOf(compilerExecutableAbsolutePath) + isolateCommandLineArgumentsToArgumentFile(arguments) } diff --git a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/impl/FieldIdStrategies.kt b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/impl/FieldIdStrategies.kt index a29a58c026..8ce32fdbc2 100644 --- a/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/impl/FieldIdStrategies.kt +++ b/utbot-framework-api/src/main/kotlin/org/utbot/framework/plugin/api/impl/FieldIdStrategies.kt @@ -47,7 +47,7 @@ class FieldIdReflectionStrategy(val fieldId: FieldId) : FieldIdStrategy { get() = Modifier.isStatic(fieldId.field.modifiers) override val isSynthetic: Boolean - get() = false + get() = fieldId.field.isSynthetic override val type: ClassId get() = fieldId.field.type.id diff --git a/utbot-framework/build.gradle b/utbot-framework/build.gradle index a558f2daf5..98a734f37b 100644 --- a/utbot-framework/build.gradle +++ b/utbot-framework/build.gradle @@ -8,20 +8,18 @@ repositories { configurations { z3native - fetchInstrumentationJar } dependencies { - fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive') api project(':utbot-core') + api project(':utbot-instrumentation') + api project(':utbot-summary') implementation 'junit:junit:4.13.1' api project(':utbot-framework-api') implementation "com.github.UnitTestBot:soot:${soot_commit_hash}" - api group: 'com.google.guava', name: 'guava', version: guava_version - implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-kotlin', version: jackson_version implementation group: 'org.sosy-lab', name: 'javasmt-solver-z3', version: javasmt_solver_z3_version implementation group: 'com.github.curious-odd-man', name: 'rgxgen', version: rgxgen_version @@ -32,7 +30,6 @@ dependencies { // we need this for construction mocks from composite models implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' api project(':utbot-api') - api project(':utbot-instrumentation') api project(':utbot-fuzzers') testImplementation project(':utbot-summary') @@ -47,12 +44,14 @@ dependencies { testImplementation group: 'org.mockito', name: 'mockito-core', version: mockito_version testImplementation group: 'org.testng', name: 'testng', version: testng_version testImplementation group: 'org.mockito', name: 'mockito-inline', version: mockito_inline_version + testImplementation group: 'com.google.guava', name: 'guava', version: guava_version testCompile group: 'org.mockito', name: 'mockito-inline', version: mockito_inline_version testCompile group: 'org.apache.logging.log4j', name: 'log4j-core', version: log4j2_version z3native group: 'com.microsoft.z3', name: 'z3-native-win64', version: z3_version, ext: 'zip' z3native group: 'com.microsoft.z3', name: 'z3-native-linux64', version: z3_version, ext: 'zip' + z3native group: 'com.microsoft.z3', name: 'z3-native-osx', version: z3_version, ext: 'zip' } processResources { @@ -61,15 +60,10 @@ processResources { into "lib/x64" } } - - // We will extract this jar in `ChildProcessRunner` class. - from(configurations.fetchInstrumentationJar) { - into "instrumentation-lib" - } } test { if (System.getProperty('DEBUG', 'false') == 'true') { jvmArgs '-Xdebug', '-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9009' } -} \ No newline at end of file +} diff --git a/utbot-framework/dist/z3-native-osx-4.8.9.1.zip b/utbot-framework/dist/z3-native-osx-4.8.9.1.zip new file mode 100644 index 0000000000..9b42561fc9 Binary files /dev/null and b/utbot-framework/dist/z3-native-osx-4.8.9.1.zip differ diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/Byte.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/Byte.java index e4e9c8b342..2b34640b25 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/Byte.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/Byte.java @@ -7,6 +7,7 @@ import static org.utbot.api.mock.UtMock.assume; import static org.utbot.engine.overrides.UtLogicMock.ite; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.overrides.UtLogicMock.less; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; @@ -34,7 +35,7 @@ public static byte parseByte(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); // we need two branches to add more options for concrete executor to find both branches if (s.charAt(0) == '-') { executeConcretely(); diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/Integer.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/Integer.java index 0c08c5d869..1142f01606 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/Integer.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/Integer.java @@ -5,7 +5,7 @@ import org.utbot.engine.overrides.strings.UtString; import org.utbot.engine.overrides.strings.UtStringBuilder; -import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.overrides.UtLogicMock.ite; import static org.utbot.engine.overrides.UtLogicMock.less; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; @@ -34,7 +34,7 @@ public static int parseInt(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); // we need two branches to add more options for concrete executor to find both branches if (s.charAt(0) == '-') { executeConcretely(); @@ -62,7 +62,7 @@ public static int parseUnsignedInt(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); if (s.charAt(0) == '-') { throw new NumberFormatException(); } else { @@ -77,8 +77,8 @@ public static String toString(int i) { } // assumes are placed here to limit search space of solver // and reduce time of solving queries with bv2int expressions - assume(i <= 0x8000); - assume(i >= -0x8000); + assumeOrExecuteConcretely(i <= 0x8000); + assumeOrExecuteConcretely(i >= -0x8000); // condition = i < 0 boolean condition = less(i, 0); // prefix = condition ? "-" : "" diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/Long.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/Long.java index 9b7a9fcdb5..4ee5283716 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/Long.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/Long.java @@ -5,7 +5,7 @@ import org.utbot.engine.overrides.strings.UtString; import org.utbot.engine.overrides.strings.UtStringBuilder; -import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.overrides.UtLogicMock.ite; import static org.utbot.engine.overrides.UtLogicMock.less; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; @@ -34,7 +34,7 @@ public static long parseLong(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); // we need two branches to add more options for concrete executor to find both branches if (s.charAt(0) == '-') { executeConcretely(); @@ -62,7 +62,7 @@ public static long parseUnsignedLong(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); if (s.charAt(0) == '-') { throw new NumberFormatException(); } else { @@ -77,8 +77,8 @@ public static String toString(long l) { } // assumes are placed here to limit search space of solver // and reduce time of solving queries with bv2int expressions - assume(l <= 10000); - assume(l >= 10000); + assumeOrExecuteConcretely(l <= 10000); + assumeOrExecuteConcretely(l >= -10000); // condition = l < 0 boolean condition = less(l, 0); // prefix = condition ? "-" : "" diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/Short.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/Short.java index af7263b898..d8b47fbaf3 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/Short.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/Short.java @@ -5,7 +5,7 @@ import org.utbot.engine.overrides.strings.UtString; import org.utbot.engine.overrides.strings.UtStringBuilder; -import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.overrides.UtLogicMock.ite; import static org.utbot.engine.overrides.UtLogicMock.less; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; @@ -34,7 +34,7 @@ public static java.lang.Short parseShort(String s, int radix) { if ((s.charAt(0) == '-' || s.charAt(0) == '+') && s.length() == 1) { throw new NumberFormatException(); } - assume(s.length() <= 10); + assumeOrExecuteConcretely(s.length() <= 10); // we need two branches to add more options for concrete executor to find both branches if (s.charAt(0) == '-') { executeConcretely(); @@ -50,8 +50,8 @@ public static String toString(short s) { boolean condition = less(s, (short) 0); // assumes are placed here to limit search space of solver // and reduce time of solving queries with bv2int expressions - assume(s <= 10000); - assume(s >= 10000); + assumeOrExecuteConcretely(s <= 10000); + assumeOrExecuteConcretely(s >= -10000); // prefix = condition ? "-" : "" String prefix = ite(condition, "-", ""); UtStringBuilder sb = new UtStringBuilder(prefix); diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/AbstractCollection.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/AbstractCollection.java new file mode 100644 index 0000000000..82ba954be4 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/AbstractCollection.java @@ -0,0 +1,13 @@ +package org.utbot.engine.overrides.collections; + +import org.utbot.api.annotation.UtClassMock; + +import static org.utbot.api.mock.UtMock.makeSymbolic; + +@UtClassMock(target = java.util.AbstractCollection.class, internalUsage = true) +public abstract class AbstractCollection implements java.util.Collection { + @Override + public String toString() { + return makeSymbolic(); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/Collection.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/Collection.java new file mode 100644 index 0000000000..8f7edebe37 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/Collection.java @@ -0,0 +1,27 @@ +package org.utbot.engine.overrides.collections; + +import org.utbot.api.annotation.UtClassMock; +import org.utbot.engine.overrides.stream.UtStream; + +import java.util.stream.Stream; + +@UtClassMock(target = java.util.Collection.class, internalUsage = true) +public interface Collection extends java.util.Collection { + @SuppressWarnings("unchecked") + @Override + default Stream parallelStream() { + Object[] data = toArray(); + int size = data.length; + + return new UtStream<>((E[]) data, size); + } + + @SuppressWarnings("unchecked") + @Override + default Stream stream() { + Object[] data = toArray(); + int size = data.length; + + return new UtStream<>((E[]) data, size); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/List.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/List.java new file mode 100644 index 0000000000..b04c16694b --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/List.java @@ -0,0 +1,72 @@ +package org.utbot.engine.overrides.collections; + +import org.utbot.api.annotation.UtClassMock; + +import java.util.Collection; + +@UtClassMock(target = java.util.List.class, internalUsage = true) +public interface List extends java.util.List { + static java.util.List of() { + return new UtArrayList<>(); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1) { + return new UtArrayList<>((E[]) new Object[]{e1}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2) { + return new UtArrayList<>((E[]) new Object[]{e1, e2}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5, E e6) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5, e6}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5, e6, e7}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5, e6, e7, e8}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5, e6, e7, e8, e9}); + } + + @SuppressWarnings("unchecked") + static java.util.List of(E e1, E e2, E e3, E e4, E e5, E e6, E e7, E e8, E e9, E e10) { + return new UtArrayList<>((E[]) new Object[]{e1, e2, e3, e4, e5, e6, e7, e8, e9, e10}); + } + + @SafeVarargs + @SuppressWarnings("varargs") + static java.util.List of(E... elements) { + return new UtArrayList<>(elements); + } + + static java.util.List copyOf(Collection collection) { + return new UtArrayList<>(collection); + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtArrayList.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtArrayList.java index 365c6d52c3..5db94b3fb7 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtArrayList.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtArrayList.java @@ -13,9 +13,13 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; +import java.util.stream.Stream; + import org.jetbrains.annotations.NotNull; +import org.utbot.engine.overrides.stream.UtStream; import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.ResolverKt.MAX_LIST_SIZE; import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; @@ -59,6 +63,14 @@ public UtArrayList(Collection c) { addAll(c); } + public UtArrayList(E[] data) { + visit(this); + int length = data.length; + elementData = new RangeModifiableUnlimitedArray<>(); + elementData.setRange(0, data, 0, length); + elementData.end = length; + } + /** * Precondition check is called only once by object, * if it was passed as parameter to method under test. @@ -82,7 +94,8 @@ void preconditionCheck() { int size = elementData.end; assume(elementData.begin == 0); - assume(size >= 0 & size <= MAX_LIST_SIZE); + assume(size >= 0); + assumeOrExecuteConcretely(size <= MAX_LIST_SIZE); visit(this); } @@ -359,6 +372,28 @@ public void replaceAll(UnaryOperator operator) { } } + @SuppressWarnings("unchecked") + @Override + public Stream stream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + + @SuppressWarnings("unchecked") + @Override + public Stream parallelStream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + /** * Auxiliary method, that should be only executed concretely * @return new ArrayList with all the elements from this. diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashMap.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashMap.java index 8e8e296493..57aba7f5c4 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashMap.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashMap.java @@ -15,9 +15,9 @@ import org.jetbrains.annotations.Nullable; import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.makeSymbolic; import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; import static org.utbot.engine.overrides.UtOverrideMock.doesntThrow; -import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; import static org.utbot.engine.overrides.UtOverrideMock.parameter; import static org.utbot.engine.overrides.UtOverrideMock.visit; @@ -100,12 +100,12 @@ void preconditionCheck() { parameter(values.touched); parameter(values.storage); parameter(keys); - // for some unknown reason parameter(keys.storage) leads to MapValuesTest::testIteratorNext test failure + parameter(keys.storage); assume(values.size == keys.end); assume(values.touched.length == keys.end); doesntThrow(); - for (int i = 0; i < keys.end; i++) { + for (int i = keys.begin; i < keys.end; i++) { K key = keys.get(i); assume(values.touched[i] == key); @@ -556,8 +556,7 @@ public final boolean remove(Object o) { // TODO rewrite it JIRA:1604 @Override public String toString() { - executeConcretely(); - return super.toString(); + return makeSymbolic(); } public final class Entry implements Map.Entry { diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashSet.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashSet.java index 744a1b5f15..69dd700e22 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashSet.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtHashSet.java @@ -8,7 +8,10 @@ import java.util.Objects; import java.util.function.Consumer; import java.util.function.Predicate; +import java.util.stream.Stream; + import org.jetbrains.annotations.NotNull; +import org.utbot.engine.overrides.stream.UtStream; import static org.utbot.api.mock.UtMock.assume; import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; @@ -73,7 +76,7 @@ void preconditionCheck() { doesntThrow(); // check that all elements are distinct. - for (int i = 0; i < elementData.end; i++) { + for (int i = elementData.begin; i < elementData.end; i++) { E element = elementData.get(i); parameter(element); // make element address non-positive @@ -263,6 +266,28 @@ public Iterator iterator() { return new UtHashSetIterator(); } + @SuppressWarnings("unchecked") + @Override + public Stream stream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + + @SuppressWarnings("unchecked") + @Override + public Stream parallelStream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + public class UtHashSetIterator implements Iterator { int index = 0; diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtLinkedList.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtLinkedList.java index 1e6be282dc..55a577cb52 100644 --- a/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtLinkedList.java +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/collections/UtLinkedList.java @@ -13,12 +13,16 @@ import java.util.function.Consumer; import java.util.function.Predicate; import java.util.function.UnaryOperator; +import java.util.stream.Stream; + import org.jetbrains.annotations.NotNull; +import org.utbot.engine.overrides.stream.UtStream; import static org.utbot.api.mock.UtMock.assume; import static org.utbot.engine.ResolverKt.MAX_LIST_SIZE; import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; import static org.utbot.engine.overrides.UtOverrideMock.parameter; import static org.utbot.engine.overrides.UtOverrideMock.visit; @@ -78,7 +82,8 @@ private void preconditionCheck() { parameter(elementData); parameter(elementData.storage); assume(elementData.begin == 0); - assume(elementData.end >= 0 & elementData.end <= MAX_LIST_SIZE); + assume(elementData.end >= 0); + assumeOrExecuteConcretely(elementData.end <= MAX_LIST_SIZE); visit(this); } @@ -446,6 +451,29 @@ public Iterator descendingIterator() { preconditionCheck(); return new ReverseIteratorWrapper(elementData.end); } + + @SuppressWarnings("unchecked") + @Override + public Stream stream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + + @SuppressWarnings("unchecked") + @Override + public Stream parallelStream() { + preconditionCheck(); + + int size = elementData.end; + Object[] data = elementData.toArray(0, size); + + return new UtStream<>((E[]) data, size); + } + public class ReverseIteratorWrapper implements ListIterator { int index; diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Arrays.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Arrays.java new file mode 100644 index 0000000000..9534452ff9 --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Arrays.java @@ -0,0 +1,29 @@ +package org.utbot.engine.overrides.stream; + +import org.utbot.api.annotation.UtClassMock; +import org.utbot.engine.overrides.collections.UtArrayList; + +import java.util.List; +import java.util.stream.Stream; + +@UtClassMock(target = java.util.Arrays.class, internalUsage = true) +public class Arrays { + public static Stream stream(T[] array, int startInclusive, int endExclusive) { + int size = array.length; + + if (startInclusive < 0 || endExclusive < startInclusive || endExclusive > size) { + throw new ArrayIndexOutOfBoundsException(); + } + + return new UtStream<>(array, startInclusive, endExclusive); + } + + @SuppressWarnings({"unused", "varargs"}) + @SafeVarargs + public static List asList(T... a) { + // TODO immutable collection https://github.com/UnitTestBot/UTBotJava/issues/398 + return new UtArrayList<>(a); + } + + // TODO primitive arrays https://github.com/UnitTestBot/UTBotJava/issues/146 +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Stream.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Stream.java new file mode 100644 index 0000000000..cf9b533a4d --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/Stream.java @@ -0,0 +1,54 @@ +package org.utbot.engine.overrides.stream; + +import org.utbot.api.annotation.UtClassMock; + +import java.util.function.Supplier; +import java.util.function.UnaryOperator; +import java.util.stream.BaseStream; + +import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; + +@SuppressWarnings("unused") +@UtClassMock(target = java.util.stream.Stream.class, internalUsage = true) +public interface Stream extends BaseStream> { + @SuppressWarnings("unchecked") + static java.util.stream.Stream of(E element) { + Object[] data = new Object[1]; + data[0] = element; + + return new UtStream<>((E[]) data, 1); + } + + @SuppressWarnings("unchecked") + static java.util.stream.Stream of(E... elements) { + int size = elements.length; + + return new UtStream<>(elements, size); + } + + @SuppressWarnings("unchecked") + static java.util.stream.Stream empty() { + return new UtStream<>((E[]) new Object[]{}, 0); + } + + static java.util.stream.Stream generate(Supplier s) { + // as "generate" method produces an infinite stream, we cannot analyze it symbolically + executeConcretely(); + return null; + } + + static java.util.stream.Stream iterate(final E seed, final UnaryOperator f) { + // as "iterate" method produces an infinite stream, we cannot analyze it symbolically + executeConcretely(); + return null; + } + + static java.util.stream.Stream concat( + java.util.stream.Stream a, + java.util.stream.Stream b + ) { + // as provided streams might be infinite, we cannot analyze this method symbolically + executeConcretely(); + return null; + } +} diff --git a/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtStream.java b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtStream.java new file mode 100644 index 0000000000..790a7ef16e --- /dev/null +++ b/utbot-framework/src/main/java/org/utbot/engine/overrides/stream/UtStream.java @@ -0,0 +1,689 @@ +package org.utbot.engine.overrides.stream; + +import org.utbot.engine.overrides.UtArrayMock; +import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray; +import org.utbot.engine.overrides.collections.UtGenericStorage; + +import java.util.Comparator; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.Supplier; +import java.util.function.ToDoubleFunction; +import java.util.function.ToIntFunction; +import java.util.function.ToLongFunction; +import java.util.stream.Collector; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; + +import static org.utbot.api.mock.UtMock.assume; +import static org.utbot.api.mock.UtMock.assumeOrExecuteConcretely; +import static org.utbot.engine.ResolverKt.HARD_MAX_ARRAY_SIZE; +import static org.utbot.engine.overrides.UtOverrideMock.alreadyVisited; +import static org.utbot.engine.overrides.UtOverrideMock.executeConcretely; +import static org.utbot.engine.overrides.UtOverrideMock.parameter; +import static org.utbot.engine.overrides.UtOverrideMock.visit; + +public class UtStream implements Stream, UtGenericStorage { + private final RangeModifiableUnlimitedArray elementData; + + private final RangeModifiableUnlimitedArray closeHandlers = new RangeModifiableUnlimitedArray<>(); + + private boolean isParallel = false; + + /** + * {@code false} by default, assigned to {@code true} after performing any operation on this stream. Any operation, + * performed on a closed UtStream, throws the {@link IllegalStateException}. + */ + private boolean isClosed = false; + + public UtStream() { + visit(this); + elementData = new RangeModifiableUnlimitedArray<>(); + } + + public UtStream(E[] data, int length) { + visit(this); + elementData = new RangeModifiableUnlimitedArray<>(); + elementData.setRange(0, data, 0, length); + elementData.end = length; + } + + public UtStream(E[] data, int startInclusive, int endExclusive) { + visit(this); + + int size = endExclusive - startInclusive; + + elementData = new RangeModifiableUnlimitedArray<>(); + elementData.setRange(0, data, startInclusive, size); + elementData.end = size; + } + + /** + * Precondition check is called only once by object, + * if it was passed as parameter to method under test. + *

+ * Preconditions that are must be satisfied: + *

  • elementData.size in 0..HARD_MAX_ARRAY_SIZE.
  • + *
  • elementData is marked as parameter
  • + *
  • elementData.storage and it's elements are marked as parameters
  • + */ + @SuppressWarnings("DuplicatedCode") + void preconditionCheck() { + if (alreadyVisited(this)) { + return; + } + setEqualGenericType(elementData); + + assume(elementData != null); + assume(elementData.storage != null); + + parameter(elementData); + parameter(elementData.storage); + + assume(elementData.begin == 0); + + assume(elementData.end >= 0); + // we can create a stream for an array using Stream.of + assume(elementData.end <= HARD_MAX_ARRAY_SIZE); + + visit(this); + } + + private void preconditionCheckWithClosingStream() { + preconditionCheck(); + + if (isClosed) { + throw new IllegalStateException(); + } + + // Even if exception occurs in the body of a stream operation, this stream could not be used later. + isClosed = true; + } + + @SuppressWarnings("unchecked") + @Override + public Stream filter(Predicate predicate) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + Object[] filtered = new Object[size]; + int j = 0; + for (int i = 0; i < size; i++) { + E element = elementData.get(i); + if (predicate.test(element)) { + filtered[j++] = element; + } + } + + return new UtStream<>((E[]) filtered, j); + } + + @SuppressWarnings("unchecked") + @Override + public Stream map(Function mapper) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + Object[] mapped = new Object[size]; + for (int i = 0; i < size; i++) { + mapped[i] = mapper.apply(elementData.get(i)); + } + + return new UtStream<>((R[]) mapped, size); + } + + @Override + public IntStream mapToInt(ToIntFunction mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @Override + public LongStream mapToLong(ToLongFunction mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @Override + public DoubleStream mapToDouble(ToDoubleFunction mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @Override + public Stream flatMap(Function> mapper) { + preconditionCheckWithClosingStream(); + // as mapper can produce infinite streams, we cannot process it symbolically + executeConcretely(); + return null; + } + + @Override + public IntStream flatMapToInt(Function mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @Override + public LongStream flatMapToLong(Function mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @Override + public DoubleStream flatMapToDouble(Function mapper) { + preconditionCheckWithClosingStream(); + // TODO https://github.com/UnitTestBot/UTBotJava/issues/146 + executeConcretely(); + return null; + } + + @SuppressWarnings("unchecked") + @Override + public Stream distinct() { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + Object[] distinctElements = new Object[size]; + int distinctSize = 0; + for (int i = 0; i < size; i++) { + E element = elementData.get(i); + boolean isDuplicate = false; + + if (element == null) { + for (int j = 0; j < distinctSize; j++) { + Object alreadyProcessedElement = distinctElements[j]; + if (alreadyProcessedElement == null) { + isDuplicate = true; + break; + } + } + } else { + for (int j = 0; j < distinctSize; j++) { + Object alreadyProcessedElement = distinctElements[j]; + if (element.equals(alreadyProcessedElement)) { + isDuplicate = true; + break; + } + } + } + + if (!isDuplicate) { + distinctElements[distinctSize++] = element; + } + } + + return new UtStream<>((E[]) distinctElements, distinctSize); + } + + // TODO choose the best sorting https://github.com/UnitTestBot/UTBotJava/issues/188 + @SuppressWarnings("unchecked") + @Override + public Stream sorted() { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + + if (size == 0) { + return new UtStream<>(); + } + + Object[] sortedElements = UtArrayMock.copyOf(elementData.toArray(0, size), size); + + // bubble sort + for (int i = 0; i < size - 1; i++) { + for (int j = 0; j < size - i - 1; j++) { + if (((Comparable) sortedElements[j]).compareTo((E) sortedElements[j + 1]) > 0) { + Object tmp = sortedElements[j]; + sortedElements[j] = sortedElements[j + 1]; + sortedElements[j + 1] = tmp; + } + } + } + + return new UtStream<>((E[]) sortedElements, size); + } + + // TODO choose the best sorting https://github.com/UnitTestBot/UTBotJava/issues/188 + @SuppressWarnings("unchecked") + @Override + public Stream sorted(Comparator comparator) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + + if (size == 0) { + return new UtStream<>(); + } + + Object[] sortedElements = UtArrayMock.copyOf(elementData.toArray(0, size), size); + + // bubble sort + for (int i = 0; i < size - 1; i++) { + for (int j = 0; j < size - i - 1; j++) { + if (comparator.compare((E) sortedElements[j], (E) sortedElements[j + 1]) > 0) { + Object tmp = sortedElements[j]; + sortedElements[j] = sortedElements[j + 1]; + sortedElements[j + 1] = tmp; + } + } + } + + return new UtStream<>((E[]) sortedElements, size); + } + + @Override + public Stream peek(Consumer action) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + for (int i = 0; i < size; i++) { + action.accept(elementData.get(i)); + } + + // returned stream should be opened, so we "reopen" this stream to return it + isClosed = false; + + return this; + } + + @SuppressWarnings("unchecked") + @Override + public Stream limit(long maxSize) { + preconditionCheckWithClosingStream(); + + if (maxSize < 0) { + throw new IllegalArgumentException(); + } + + assumeOrExecuteConcretely(maxSize <= Integer.MAX_VALUE); + + int newSize = (int) maxSize; + int curSize = elementData.end; + + if (newSize == curSize) { + return this; + } + + if (newSize > curSize) { + newSize = curSize; + } + + return new UtStream<>((E[]) elementData.toArray(0, newSize), newSize); + } + + @SuppressWarnings("unchecked") + @Override + public Stream skip(long n) { + preconditionCheckWithClosingStream(); + + if (n < 0) { + throw new IllegalArgumentException(); + } + + if (n == 0) { + return this; + } + + int curSize = elementData.end; + if (n > curSize) { + return new UtStream<>(); + } + + // n is 1...Integer.MAX_VALUE here + int newSize = (int) (curSize - n); + + return new UtStream<>((E[]) elementData.toArray((int) n, newSize), newSize); + } + + @Override + public void forEach(Consumer action) { + peek(action); + } + + @Override + public void forEachOrdered(Consumer action) { + peek(action); + } + + @NotNull + @Override + public Object[] toArray() { + preconditionCheckWithClosingStream(); + + return elementData.toArray(0, elementData.end); + } + + @NotNull + @Override + public A[] toArray(IntFunction generator) { + preconditionCheckWithClosingStream(); + + // TODO untracked ArrayStoreException - JIRA:1089 + int size = elementData.end; + A[] array = generator.apply(size); + + UtArrayMock.arraycopy(elementData.toArray(0, size), 0, array, 0, size); + + return array; + } + + @Override + public E reduce(E identity, BinaryOperator accumulator) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + E result = identity; + for (int i = 0; i < size; i++) { + result = accumulator.apply(result, elementData.get(i)); + } + + return result; + } + + @SuppressWarnings("ConstantConditions") + @NotNull + @Override + public Optional reduce(BinaryOperator accumulator) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + if (size == 0) { + return Optional.empty(); + } + + E result = null; + for (int i = 0; i < size; i++) { + E element = elementData.get(i); + if (result == null) { + result = element; + } else { + result = accumulator.apply(result, element); + } + } + + return Optional.of(result); + } + + @Override + public U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) { + preconditionCheckWithClosingStream(); + + // since this implementation is always sequential, we do not need to use the combiner + int size = elementData.end; + U result = identity; + for (int i = 0; i < size; i++) { + result = accumulator.apply(result, elementData.get(i)); + } + + return result; + } + + @Override + public R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) { + preconditionCheckWithClosingStream(); + + // since this implementation is always sequential, we do not need to use the combiner + int size = elementData.end; + R result = supplier.get(); + for (int i = 0; i < size; i++) { + accumulator.accept(result, elementData.get(i)); + } + + return result; + } + + @Override + public R collect(Collector collector) { + preconditionCheckWithClosingStream(); + + // since this implementation is always sequential, we do not need to use the combiner + int size = elementData.end; + A result = collector.supplier().get(); + for (int i = 0; i < size; i++) { + collector.accumulator().accept(result, elementData.get(i)); + } + + return collector.finisher().apply(result); + } + + @NotNull + @Override + public Optional min(Comparator comparator) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + if (size == 0) { + return Optional.empty(); + } + + E min = elementData.get(0); + for (int i = 1; i < size; i++) { + E element = elementData.get(i); + if (comparator.compare(min, element) > 0) { + min = element; + } + } + + return Optional.of(min); + } + + @NotNull + @Override + public Optional max(Comparator comparator) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + if (size == 0) { + return Optional.empty(); + } + + E max = elementData.get(0); + for (int i = 1; i < size; i++) { + E element = elementData.get(i); + if (comparator.compare(max, element) < 0) { + max = element; + } + } + + return Optional.of(max); + } + + @Override + public long count() { + preconditionCheckWithClosingStream(); + + return elementData.end; + } + + @Override + public boolean anyMatch(Predicate predicate) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + for (int i = 0; i < size; i++) { + if (predicate.test(elementData.get(i))) { + return true; + } + } + + return false; + } + + @Override + public boolean allMatch(Predicate predicate) { + preconditionCheckWithClosingStream(); + + int size = elementData.end; + for (int i = 0; i < size; i++) { + if (!predicate.test(elementData.get(i))) { + return false; + } + } + + return true; + } + + @Override + public boolean noneMatch(Predicate predicate) { + preconditionCheckWithClosingStream(); + + return !anyMatch(predicate); + } + + @NotNull + @Override + public Optional findFirst() { + preconditionCheckWithClosingStream(); + + if (elementData.end == 0) { + return Optional.empty(); + } + + E first = elementData.get(0); + + return Optional.of(first); + } + + @NotNull + @Override + public Optional findAny() { + preconditionCheckWithClosingStream(); + + // since this implementation is always sequential, we can just return the first element + return findFirst(); + } + + @NotNull + @Override + public Iterator iterator() { + preconditionCheckWithClosingStream(); + + return new UtStreamIterator(0); + } + + @NotNull + @Override + public Spliterator spliterator() { + preconditionCheckWithClosingStream(); + // implementation from AbstractList + return Spliterators.spliterator(elementData.toArray(0, elementData.end), Spliterator.ORDERED); + } + + @Override + public boolean isParallel() { + // this method does not "close" this stream + preconditionCheck(); + + return isParallel; + } + + @NotNull + @Override + public Stream sequential() { + // this method does not "close" this stream + preconditionCheck(); + + isParallel = false; + + return this; + } + + @NotNull + @Override + public Stream parallel() { + // this method does not "close" this stream + preconditionCheck(); + + isParallel = true; + + return this; + } + + @NotNull + @Override + public Stream unordered() { + // this method does not "close" this stream + preconditionCheck(); + + return this; + } + + @NotNull + @Override + public Stream onClose(Runnable closeHandler) { + // this method does not "close" this stream + preconditionCheck(); + + // adds closeHandler to existing + closeHandlers.set(closeHandlers.end++, closeHandler); + + return this; + } + + @Override + public void close() { + // Stream can be closed via this method many times + preconditionCheck(); + + // TODO resources closing https://github.com/UnitTestBot/UTBotJava/issues/189 + + // NOTE: this implementation does not care about suppressing and throwing exceptions produced by handlers + for (int i = 0; i < closeHandlers.end; i++) { + closeHandlers.get(i).run(); + } + } + + public class UtStreamIterator implements Iterator { + int index; + + UtStreamIterator(int index) { + if (index < 0 || index > elementData.end) { + throw new IndexOutOfBoundsException(); + } + + this.index = index; + } + + @Override + public boolean hasNext() { + preconditionCheck(); + + return index != elementData.end; + } + + @Override + public E next() { + preconditionCheck(); + + if (index == elementData.end) { + throw new NoSuchElementException(); + } + + return elementData.get(index++); + } + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/EngineAnalyticsContext.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/EngineAnalyticsContext.kt new file mode 100644 index 0000000000..b5624e8874 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/EngineAnalyticsContext.kt @@ -0,0 +1,36 @@ +package org.utbot.analytics + +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.selectors.NNRewardGuidedSelectorFactory +import org.utbot.engine.selectors.NNRewardGuidedSelectorWithRecalculationFactory +import org.utbot.engine.selectors.NNRewardGuidedSelectorWithoutRecalculationFactory +import org.utbot.framework.NNRewardGuidedSelectorType +import org.utbot.framework.UtSettings + +/** + * Class that stores all objects that need for work analytics module during symbolic execution + */ +object EngineAnalyticsContext { + var featureProcessorFactory: FeatureProcessorFactory = object : FeatureProcessorFactory { + override fun invoke(graph: InterProceduralUnitGraph): FeatureProcessor { + error("Feature processor factory is not provided.") + } + } + + var featureExtractorFactory: FeatureExtractorFactory = object : FeatureExtractorFactory { + override fun invoke(graph: InterProceduralUnitGraph): FeatureExtractor { + error("Feature extractor factory is not provided.") + } + } + + val nnRewardGuidedSelectorFactory: NNRewardGuidedSelectorFactory = when (UtSettings.nnRewardGuidedSelectorType) { + NNRewardGuidedSelectorType.WITHOUT_RECALCULATION -> NNRewardGuidedSelectorWithoutRecalculationFactory() + NNRewardGuidedSelectorType.WITH_RECALCULATION -> NNRewardGuidedSelectorWithRecalculationFactory() + } + + var stateRewardPredictorFactory: StateRewardPredictorFactory = object : StateRewardPredictorFactory { + override fun invoke(): StateRewardPredictor { + error("NNStateRewardPredictor factory wasn't provided") + } + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt new file mode 100644 index 0000000000..c86cad1b54 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractor.kt @@ -0,0 +1,14 @@ +package org.utbot.analytics + +import org.utbot.engine.ExecutionState + +/** + * Class that encapsulates work with FeatureExtractor during symbolic execution. + */ +interface FeatureExtractor { + /** + * Extract features and store in it [ExecutionState.features] + * @param generatedTestCases number of generated tests so far + */ + fun extractFeatures(executionState: ExecutionState, generatedTestCases: Int) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractorFactory.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractorFactory.kt new file mode 100644 index 0000000000..6ad7a916a6 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureExtractorFactory.kt @@ -0,0 +1,10 @@ +package org.utbot.analytics + +import org.utbot.engine.InterProceduralUnitGraph + +/** + * Class that can create FeatureExtractor. See [FeatureExtractor]. + */ +interface FeatureExtractorFactory { + operator fun invoke(graph: InterProceduralUnitGraph): FeatureExtractor +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessor.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessor.kt new file mode 100644 index 0000000000..e69e783a97 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessor.kt @@ -0,0 +1,14 @@ +package org.utbot.analytics + +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.selectors.strategies.TraverseGraphStatistics + +/** + * Interface that encapsulates work with FeatureProcessor and can only dumpFeatures at the end of symbolic execution + */ +abstract class FeatureProcessor(graph: InterProceduralUnitGraph) : TraverseGraphStatistics(graph) { + /** + * Dump features and rewards of all states in collected test cases. + */ + abstract fun dumpFeatures() +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessorFactory.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessorFactory.kt new file mode 100644 index 0000000000..d012aa7e89 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/FeatureProcessorFactory.kt @@ -0,0 +1,10 @@ +package org.utbot.analytics + +import org.utbot.engine.InterProceduralUnitGraph + +/** + * Class that can create FeatureProcessor. See [FeatureProcessor] + */ +interface FeatureProcessorFactory { + operator fun invoke(graph: InterProceduralUnitGraph): FeatureProcessor +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/Predictors.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/Predictors.kt index d7876049cb..562d6db7c3 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/analytics/Predictors.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/Predictors.kt @@ -1,7 +1,6 @@ package org.utbot.analytics import org.utbot.engine.pc.UtExpression -import kotlinx.collections.immutable.PersistentList import soot.jimple.Stmt /** @@ -18,9 +17,16 @@ object Predictors { * Predict z3's [Solver.check()] execution time in nanoseconds */ var smt: UtBotNanoTimePredictor> = object : UtBotNanoTimePredictor> {} - var smtIncremental: UtBotNanoTimePredictor = object : UtBotNanoTimePredictor {} + var smtIncremental: UtBotNanoTimePredictor = object : UtBotNanoTimePredictor {} var testName: UtBotAbstractPredictor, String> = object : UtBotAbstractPredictor, String> { override fun predict(input: Iterable): String = "stubName" } + + var stateRewardPredictor: UtBotAbstractPredictor, Double> = + object : UtBotAbstractPredictor, Double> { + override fun predict(input: List): Double { + error("stateRewardPredictor is not provided.") + } + } } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictor.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictor.kt new file mode 100644 index 0000000000..bcb55a7eeb --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictor.kt @@ -0,0 +1,6 @@ +package org.utbot.analytics + +/** + * Interface, which should predict reward for state by features list. + */ +interface StateRewardPredictor : UtBotAbstractPredictor, Double> \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictorFactory.kt b/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictorFactory.kt new file mode 100644 index 0000000000..60d1aa9b40 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/analytics/StateRewardPredictorFactory.kt @@ -0,0 +1,8 @@ +package org.utbot.analytics + +/** + * Encapsulates creation of [StateRewardPredictor] + */ +interface StateRewardPredictorFactory { + operator fun invoke(): StateRewardPredictor +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/AnnotationResolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/AnnotationResolver.kt index 31033a9eec..e6509898d6 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/AnnotationResolver.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/AnnotationResolver.kt @@ -109,12 +109,6 @@ val SootMethod.findMockAnnotationOrNull val SootMethod.hasInternalMockAnnotation get() = annotationsOrNull?.singleOrNull { it.type == utInternalUsageBytecodeSignature } != null -/** - * Returns true if the [SootMethod]'s signature is equal to [UtMock.assume]'s signature, false otherwise. - */ -val SootMethod.isUtMockAssume - get() = signature == assumeMethod.signature - val utClassMockBytecodeSignature = UtClassMock::class.java.bytecodeSignature() val utConstructorMockBytecodeSignature = UtConstructorMock::class.java.bytecodeSignature() val utInternalUsageBytecodeSignature = UtInternalUsage::class.java.bytecodeSignature() diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt index 41db4cbefa..5c53d0c751 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ArrayObjectWrappers.kt @@ -26,18 +26,17 @@ import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.UtReferenceModel import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId +import soot.ArrayType import soot.Scene +import soot.SootClass +import soot.SootField import soot.SootMethod val rangeModifiableArrayId: ClassId = RangeModifiableUnlimitedArray::class.id class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { - private val rangeModifiableArrayClass = Scene.v().getSootClass(rangeModifiableArrayId.name) - private val beginField = rangeModifiableArrayClass.getField("int begin") - private val endField = rangeModifiableArrayClass.getField("int end") - private val storageField = rangeModifiableArrayClass.getField("java.lang.Object[] storage") - override fun UtBotSymbolicEngine.invoke( wrapper: ObjectValue, method: SootMethod, @@ -168,13 +167,15 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { val typeStorage = typeResolver.constructTypeStorage(OBJECT_TYPE.arrayType, useConcreteType = false) val array = ArrayValue(typeStorage, arrayAddr) + val hardConstraints = setOf( + Eq(memory.findArrayLength(arrayAddr), length), + typeRegistry.typeConstraint(arrayAddr, array.typeStorage).all(), + ).asHardConstraint() + listOf( MethodResult( SymbolicSuccess(array), - hardConstraints = setOf( - Eq(memory.findArrayLength(arrayAddr), length), - typeRegistry.typeConstraint(array.addr, array.typeStorage).all() - ).asHardConstraint(), + hardConstraints = hardConstraints, memoryUpdates = arrayUpdateWithValue(arrayAddr, OBJECT_TYPE.arrayType, value) ) ) @@ -233,7 +234,7 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { val resultModel = UtArrayModel( concreteAddr, - objectClassId, + objectArrayClassId, sizeValue, UtNullModel(objectClassId), mutableMapOf() @@ -243,17 +244,35 @@ class RangeModifiableUnlimitedArrayWrapper : WrapperInterface { // the constructed model to avoid infinite recursion below resolver.addConstructedModel(concreteAddr, resultModel) + // try to retrieve type storage for the single type parameter + val typeStorage = + resolver.typeRegistry.getTypeStoragesForObjectTypeParameters(wrapper.addr)?.singleOrNull() ?: TypeRegistry.objectTypeStorage + (0 until sizeValue).associateWithTo(resultModel.stores) { i -> - resolver.resolveModel( - ObjectValue( - TypeStorage(OBJECT_TYPE), - UtAddrExpression(arrayExpression.select(mkInt(i + firstValue))) - ) - ) + val addr = UtAddrExpression(arrayExpression.select(mkInt(i + firstValue))) + + val value = if (typeStorage.leastCommonType is ArrayType) { + ArrayValue(typeStorage, addr) + } else { + ObjectValue(typeStorage, addr) + } + + resolver.resolveModel(value) } return resultModel } + + companion object { + internal val rangeModifiableArrayClass: SootClass + get() = Scene.v().getSootClass(rangeModifiableArrayId.name) + internal val beginField: SootField + get() = rangeModifiableArrayClass.getFieldByName("begin") + internal val endField: SootField + get() = rangeModifiableArrayClass.getFieldByName("end") + internal val storageField: SootField + get() = rangeModifiableArrayClass.getFieldByName("storage") + } } val associativeArrayId: ClassId = AssociativeArray::class.id diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt index 3b74f25a60..55a65470cf 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/CollectionWrappers.kt @@ -10,6 +10,7 @@ import org.utbot.engine.overrides.collections.UtHashSet import org.utbot.engine.overrides.collections.UtLinkedList import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtExpression +import org.utbot.engine.pc.UtFalse import org.utbot.engine.pc.select import org.utbot.engine.symbolic.asHardConstraint import org.utbot.engine.z3.intValue @@ -33,7 +34,6 @@ import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.util.nextModelName -import java.util.LinkedList import soot.IntType import soot.RefType import soot.Scene @@ -74,7 +74,6 @@ abstract class BaseOverriddenWrapper(protected val overriddenClassName: String) method: SootMethod, parameters: List ): List { - val methodResults = overrideInvoke(wrapper, method, parameters) if (methodResults != null) { return methodResults @@ -84,35 +83,57 @@ abstract class BaseOverriddenWrapper(protected val overriddenClassName: String) return listOf(GraphResult(method.jimpleBody().graph())) } - val overriddenMethod = overriddenClass.findMethodOrNull(method.subSignature) - - val jimpleBody = overriddenMethod?.jimpleBody() ?: method.jimpleBody() - val graphResult = GraphResult(jimpleBody.graph()) - - return listOf(graphResult) + // We need to find either an override from the class (for example, implementation + // of the method from the wrapper) or a method from its ancestors. + // Note that the method from the ancestor might have substitutions as well. + // I.e., it is `toString` method for `UtArrayList` that is defined in + // `AbstractCollection` and has its own overridden implementation. + val overriddenMethod = overriddenClass + .findMethodOrNull(method.subSignature) + ?.let { typeRegistry.findSubstitutionOrNull(it) ?: it } + + return if (overriddenMethod == null) { + // No overridden method has been found, switch to concrete execution + pathLogger.warn("Method ${overriddenClass.name}::${method.subSignature} not found, executing concretely") + emptyList() + } else { + val jimpleBody = overriddenMethod.jimpleBody() + val graphResult = GraphResult(jimpleBody.graph()) + listOf(graphResult) + } } } /** - * WrapperInterface that proxies an implementation with methods of [overriddenClass]. + * Wrapper for a particular [java.util.Collection] or [java.util.Map] or [java.util.stream.Stream]. */ -abstract class BaseCollectionWrapper(collectionClassName: String) : BaseOverriddenWrapper(collectionClassName) { +abstract class BaseContainerWrapper(containerClassName: String) : BaseOverriddenWrapper(containerClassName) { /** * Resolve [wrapper] to [UtAssembleModel] using [resolver]. */ override fun value(resolver: Resolver, wrapper: ObjectValue): UtAssembleModel = resolver.run { - val classId = chooseClassIdWithConstructor(wrapper.type.sootClass.id) val addr = holder.concreteAddr(wrapper.addr) val modelName = nextModelName(baseModelName) val parameterModels = resolveValueModels(wrapper) + val classId = chooseClassIdWithConstructor(wrapper.type.sootClass.id) + val instantiationChain = mutableListOf() val modificationsChain = mutableListOf() - return UtAssembleModel(addr, classId, modelName, instantiationChain, modificationsChain) + + UtAssembleModel(addr, classId, modelName, instantiationChain, modificationsChain) .apply { - instantiationChain += UtExecutableCallModel(null, constructorId(classId), emptyList(), this) - modificationsChain += parameterModels.map { UtExecutableCallModel(this, modificationMethodId, it) } + instantiationChain += UtExecutableCallModel( + instance = null, + executable = constructorId(classId), + params = emptyList(), + returnValue = this + ) + + modificationsChain += parameterModels.map { + UtExecutableCallModel(this, modificationMethodId, it) + } } } @@ -140,6 +161,36 @@ abstract class BaseCollectionWrapper(collectionClassName: String) : BaseOverridd override fun toString() = "$overriddenClassName()" } +abstract class BaseGenericStorageBasedContainerWrapper(containerClassName: String) : BaseContainerWrapper(containerClassName) { + override fun UtBotSymbolicEngine.overrideInvoke( + wrapper: ObjectValue, + method: SootMethod, + parameters: List + ): List? = + when (method.signature) { + UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> { + val equalGenericTypeConstraint = typeRegistry + .eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr) + .asHardConstraint() + + val methodResult = MethodResult( + SymbolicSuccess(voidValue), + equalGenericTypeConstraint + ) + + listOf(methodResult) + } + else -> null + } + + override fun Resolver.resolveValueModels(wrapper: ObjectValue): List> { + val elementDataFieldId = FieldId(overriddenClass.type.classId, "elementData") + val arrayModel = collectFieldModels(wrapper.addr, overriddenClass.type)[elementDataFieldId] as? UtArrayModel + + return arrayModel?.let { constructValues(arrayModel, arrayModel.length) } ?: emptyList() + } +} + /** * Auxiliary enum class for specifying an implementation for [ListWrapper], that it will use. */ @@ -165,30 +216,7 @@ enum class UtListClass { * Modification chain consists of consequent [java.util.List.add] methods * that are arranged to iterating order of list. */ -class ListWrapper(private val utListClass: UtListClass) : BaseCollectionWrapper(utListClass.className) { - override fun UtBotSymbolicEngine.overrideInvoke( - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List? = - if (method.signature == UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE) { - listOf( - MethodResult( - SymbolicSuccess(voidValue), - typeRegistry.eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr).asHardConstraint() - ) - ) - } else { - null - } - - override fun Resolver.resolveValueModels(wrapper: ObjectValue): List> { - val elementDataFieldId = FieldId(overriddenClass.type.classId, "elementData") - val arrayModel = collectFieldModels(wrapper.addr, overriddenClass.type)[elementDataFieldId] as? UtArrayModel - - return arrayModel?.let { constructValues(arrayModel, arrayModel.length) } ?: emptyList() - } - +class ListWrapper(private val utListClass: UtListClass) : BaseGenericStorageBasedContainerWrapper(utListClass.className) { /** * Chooses proper class for instantiation. Uses [java.util.ArrayList] instead of [java.util.List] * or [java.util.AbstractList]. @@ -203,7 +231,7 @@ class ListWrapper(private val utListClass: UtListClass) : BaseCollectionWrapper( classId = java.util.List::class.id, name = "add", returnType = booleanClassId, - arguments = arrayOf(java.lang.Object::class.id), + arguments = arrayOf(objectClassId), ) override val baseModelName = "list" @@ -221,31 +249,7 @@ class ListWrapper(private val utListClass: UtListClass) : BaseCollectionWrapper( * through [java.util.HashSet] in program and generated test depends on the order of * entries, then real behavior of generated test can differ from expected and undefined. */ -class SetWrapper : BaseCollectionWrapper(UtHashSet::class.qualifiedName!!) { - override fun UtBotSymbolicEngine.overrideInvoke( - wrapper: ObjectValue, - method: SootMethod, - parameters: List - ): List? = - if (method.signature == UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE) { - listOf( - MethodResult( - SymbolicSuccess(voidValue), - typeRegistry.eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr) - .asHardConstraint() - ) - ) - } else { - null - } - - override fun Resolver.resolveValueModels(wrapper: ObjectValue): List> { - val elementDataFieldId = FieldId(overriddenClass.type.classId, "elementData") - val arrayModel = collectFieldModels(wrapper.addr, overriddenClass.type)[elementDataFieldId] as? UtArrayModel - - return arrayModel?.let { constructValues(arrayModel, arrayModel.length) } ?: emptyList() - } - +class SetWrapper : BaseGenericStorageBasedContainerWrapper(UtHashSet::class.qualifiedName!!) { /** * Chooses proper class for instantiation. Uses [java.util.ArrayList] instead of [java.util.List] * or [java.util.AbstractList]. @@ -260,7 +264,7 @@ class SetWrapper : BaseCollectionWrapper(UtHashSet::class.qualifiedName!!) { classId = java.util.Set::class.id, name = "add", returnType = booleanClassId, - arguments = arrayOf(java.lang.Object::class.id), + arguments = arrayOf(objectClassId), ) override val baseModelName: String = "set" @@ -278,32 +282,32 @@ class SetWrapper : BaseCollectionWrapper(UtHashSet::class.qualifiedName!!) { * through [java.util.HashMap] in program and generated test depends on the order of * entries, then real behavior of generated test can differ from expected and undefined. */ -class MapWrapper : BaseCollectionWrapper(UtHashMap::class.qualifiedName!!) { +class MapWrapper : BaseContainerWrapper(UtHashMap::class.qualifiedName!!) { override fun UtBotSymbolicEngine.overrideInvoke( wrapper: ObjectValue, method: SootMethod, parameters: List - ): List? { - return when (method.signature) { + ): List? = + when (method.signature) { UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> listOf( MethodResult( SymbolicSuccess(voidValue), - typeRegistry.eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr).asHardConstraint() + typeRegistry.eqGenericSingleTypeParameterConstraint(parameters[0].addr, wrapper.addr) + .asHardConstraint() ) ) UT_GENERIC_ASSOCIATIVE_SET_EQUAL_GENERIC_TYPE_SIGNATURE -> listOf( MethodResult( SymbolicSuccess(voidValue), - typeRegistry.eqGenericTypeParametersConstraint( - parameters[0].addr, - wrapper.addr, - parameterSize = 2 - ).asHardConstraint() + typeRegistry.eqGenericTypeParametersConstraint( + parameters[0].addr, + wrapper.addr, + parameterSize = 2 + ).asHardConstraint() ) ) else -> null } - } override fun Resolver.resolveValueModels(wrapper: ObjectValue): List> { val fieldModels = collectFieldModels(wrapper.addr, overriddenClass.type) @@ -344,7 +348,7 @@ class MapWrapper : BaseCollectionWrapper(UtHashMap::class.qualifiedName!!) { /** * Constructs collection values using underlying array model. If model is null model, returns list of nulls. */ -private fun constructValues(model: UtModel, size: Int): List> = when (model) { +internal fun constructValues(model: UtModel, size: Int): List> = when (model) { is UtArrayModel -> List(size) { listOf(model.stores[it] ?: model.constModel) } is UtNullModel -> { val elementClassId = model.classId.elementClassId @@ -387,7 +391,7 @@ private fun constructKeysAndValues(keysModel: UtModel, valuesModel: UtModel, siz private val UT_GENERIC_STORAGE_CLASS get() = Scene.v().getSootClass(UtGenericStorage::class.java.canonicalName) -private val UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE = +internal val UT_GENERIC_STORAGE_SET_EQUAL_GENERIC_TYPE_SIGNATURE = UT_GENERIC_STORAGE_CLASS.getMethodByName(UtGenericStorage<*>::setEqualGenericType.name).signature private val UT_GENERIC_ASSOCIATIVE_CLASS @@ -397,9 +401,9 @@ private val UT_GENERIC_ASSOCIATIVE_SET_EQUAL_GENERIC_TYPE_SIGNATURE = UT_GENERIC_ASSOCIATIVE_CLASS.getMethodByName(UtGenericAssociative<*, *>::setEqualGenericType.name).signature val ARRAY_LIST_TYPE: RefType - get() = Scene.v().getSootClass(ArrayList::class.java.canonicalName).type + get() = Scene.v().getSootClass(java.util.ArrayList::class.java.canonicalName).type val LINKED_LIST_TYPE: RefType - get() = Scene.v().getSootClass(LinkedList::class.java.canonicalName).type + get() = Scene.v().getSootClass(java.util.LinkedList::class.java.canonicalName).type val LINKED_HASH_SET_TYPE: RefType get() = Scene.v().getSootClass(java.util.LinkedHashSet::class.java.canonicalName).type @@ -411,6 +415,9 @@ val LINKED_HASH_MAP_TYPE: RefType val HASH_MAP_TYPE: RefType get() = Scene.v().getSootClass(java.util.HashMap::class.java.canonicalName).type +val STREAM_TYPE: RefType + get() = Scene.v().getSootClass(java.util.stream.Stream::class.java.canonicalName).type + internal fun UtBotSymbolicEngine.getArrayField( addr: UtAddrExpression, wrapperClass: SootClass, diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt index 1d0347fb82..860d1c7d71 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/DataClasses.kt @@ -8,6 +8,7 @@ import org.utbot.engine.pc.UtIsExpression import org.utbot.engine.pc.UtTrue import org.utbot.engine.pc.mkAnd import org.utbot.engine.pc.mkOr +import org.utbot.engine.symbolic.Assumption import org.utbot.engine.symbolic.HardConstraint import org.utbot.engine.symbolic.SoftConstraint import org.utbot.engine.symbolic.SymbolicStateUpdate @@ -134,15 +135,17 @@ data class MethodResult( symbolicResult: SymbolicResult, hardConstraints: HardConstraint = HardConstraint(), softConstraints: SoftConstraint = SoftConstraint(), + assumption: Assumption = Assumption(), memoryUpdates: MemoryUpdate = MemoryUpdate() - ) : this(symbolicResult, SymbolicStateUpdate(hardConstraints, softConstraints, memoryUpdates)) + ) : this(symbolicResult, SymbolicStateUpdate(hardConstraints, softConstraints, assumption, memoryUpdates)) constructor( value: SymbolicValue, hardConstraints: HardConstraint = HardConstraint(), softConstraints: SoftConstraint = SoftConstraint(), + assumption: Assumption = Assumption(), memoryUpdates: MemoryUpdate = MemoryUpdate(), - ) : this(SymbolicSuccess(value), hardConstraints, softConstraints, memoryUpdates) + ) : this(SymbolicSuccess(value), hardConstraints, softConstraints, assumption, memoryUpdates) } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt index 79ab953a89..9c831771c0 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ExecutionState.kt @@ -1,10 +1,5 @@ package org.utbot.engine -import org.utbot.common.md5 -import org.utbot.engine.pc.UtSolver -import org.utbot.engine.symbolic.SymbolicState -import org.utbot.engine.symbolic.SymbolicStateUpdate -import org.utbot.framework.plugin.api.Step import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.PersistentSet @@ -12,9 +7,16 @@ import kotlinx.collections.immutable.persistentHashMapOf import kotlinx.collections.immutable.persistentHashSetOf import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.plus +import org.utbot.common.md5 +import org.utbot.engine.pc.UtSolver import org.utbot.engine.pc.UtSolverStatusUNDEFINED +import org.utbot.engine.symbolic.SymbolicState +import org.utbot.engine.symbolic.SymbolicStateUpdate +import org.utbot.framework.UtSettings +import org.utbot.framework.plugin.api.Step import soot.SootMethod import soot.jimple.Stmt +import java.util.Objects const val RETURN_DECISION_NUM = -1 const val CALL_DECISION_NUM = -2 @@ -40,6 +42,55 @@ data class ExecutionStackElement( this.copy(localVariableMemory = localVariableMemory.update(memoryUpdate), doesntThrow = doesntThrow) } +/** + * Class that store all information about execution state that needed only for analytics module + */ +data class StateAnalyticsProperties( + /** + * Number of forks already performed along state's path, where fork is statement with multiple successors + */ + val depth: Int = 0, + var visitedAfterLastFork: Int = 0, + var visitedBeforeLastFork: Int = 0, + var stmtsSinceLastCovered: Int = 0, + val parent: ExecutionState? = null, +) { + var executingTime: Long = 0 + var reward: Double? = null + val features: MutableList = mutableListOf() + + /** + * Flag that indicates whether this state is fork or not. Fork here means that we have more than one successor + */ + private var isFork: Boolean = false + + fun definitelyFork() { + isFork = true + } + + private var isVisitedNew: Boolean = false + + fun updateIsVisitedNew() { + isVisitedNew = true + stmtsSinceLastCovered = 0 + visitedAfterLastFork++ + } + + private val successorDepth: Int get() = depth + if (isFork) 1 else 0 + + private val successorVisitedAfterLastFork: Int get() = if (!isFork) visitedAfterLastFork else 0 + private val successorVisitedBeforeLastFork: Int get() = visitedBeforeLastFork + if (isFork) visitedAfterLastFork else 0 + private val successorStmtSinceLastCovered: Int get() = 1 + stmtsSinceLastCovered + + fun successorProperties(parent: ExecutionState) = StateAnalyticsProperties( + successorDepth, + successorVisitedAfterLastFork, + successorVisitedBeforeLastFork, + successorStmtSinceLastCovered, + if (UtSettings.enableFeatureProcess) parent else null + ) +} + /** * [visitedStatementsHashesToCountInPath] is a map representing how many times each instruction from the [path] * has occurred. It is required to calculate priority of the branches and decrease the priority for branches leading @@ -61,12 +112,14 @@ data class ExecutionState( val lastMethod: SootMethod? = null, val methodResult: MethodResult? = null, val exception: SymbolicFailure? = null, - private var outgoingEdges: Int = 0 + private var stateAnalyticsProperties: StateAnalyticsProperties = StateAnalyticsProperties() ) : AutoCloseable { val solver: UtSolver by symbolicState::solver val memory: Memory by symbolicState::memory + private var outgoingEdges = 0 + fun isInNestedMethod() = executionStack.size > 1 val localVariableMemory @@ -107,7 +160,8 @@ data class ExecutionState( pathLength = pathLength + 1, lastEdge = edge, lastMethod = executionStack.last().method, - exception = exception + exception = exception, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) ) } @@ -123,14 +177,18 @@ data class ExecutionState( symbolicState = symbolicState, executionStack = executionStack.removeAt(executionStack.lastIndex), path = path + stmt, - visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put(stmtHashcode, stmtCountInPath), + visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put( + stmtHashcode, + stmtCountInPath + ), decisionPath = decisionPath + edge.decisionNum, edges = edges + edge, stmts = stmts.putIfAbsent(stmt, pathLength), pathLength = pathLength + 1, lastEdge = edge, lastMethod = executionStack.last().method, - methodResult + methodResult, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) ) } @@ -158,13 +216,17 @@ data class ExecutionState( symbolicState = symbolicState.stateForNestedMethod() + update, executionStack = executionStack + stackElement, path = path + this.stmt, - visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put(stmtHashCode, stmtCountInPath), + visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put( + stmtHashCode, + stmtCountInPath + ), decisionPath = decisionPath + edge.decisionNum, edges = edges + edge, stmts = stmts.putIfAbsent(this.stmt, pathLength), pathLength = pathLength + 1, lastEdge = edge, - lastMethod = stackElement.method + lastMethod = stackElement.method, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) ) } @@ -173,7 +235,10 @@ data class ExecutionState( ): ExecutionState { val last = executionStack.last() val stackElement = last.update(stateUpdate.localMemoryUpdates) - return copy(symbolicState = symbolicState + stateUpdate, executionStack = executionStack.set(executionStack.lastIndex, stackElement)) + return copy( + symbolicState = symbolicState + stateUpdate, + executionStack = executionStack.set(executionStack.lastIndex, stackElement) + ) } fun update( @@ -196,13 +261,17 @@ data class ExecutionState( symbolicState = symbolicState + symbolicStateUpdate, executionStack = executionStack.set(executionStack.lastIndex, stackElement), path = path + stmt, - visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put(stmtHashCode, stmtCountInPath), + visitedStatementsHashesToCountInPath = visitedStatementsHashesToCountInPath.put( + stmtHashCode, + stmtCountInPath + ), decisionPath = decisionPath + edge.decisionNum, edges = edges + edge, stmts = stmts.putIfAbsent(stmt, pathLength), pathLength = pathLength + 1, lastEdge = edge, - lastMethod = stackElement.method + lastMethod = stackElement.method, + stateAnalyticsProperties = stateAnalyticsProperties.successorProperties(this) ) } @@ -251,14 +320,70 @@ data class ExecutionState( */ fun prettifiedPathLog(): String { val path = fullPath() - val prettifiedPath = path.joinToString(separator = "\n") { (stmt, depth, decision) -> + val prettifiedPath = prettifiedPath(path) + return " MD5(path)=${md5(prettifiedPath)}\n$prettifiedPath" + } + + private fun md5(prettifiedPath: String) = prettifiedPath.md5() + + fun md5() = prettifiedPath(fullPath()).md5() + + private fun prettifiedPath(path: List) = + path.joinToString(separator = "\n") { (stmt, depth, decision) -> val prefix = when (decision) { - CALL_DECISION_NUM -> "call[${depth}] - " + "".padEnd(2*depth, ' ') - RETURN_DECISION_NUM -> " ret[${depth - 1}] - " + "".padEnd(2*depth, ' ') - else -> " "+"".padEnd(2*depth, ' ') + CALL_DECISION_NUM -> "call[${depth}] - " + "".padEnd(2 * depth, ' ') + RETURN_DECISION_NUM -> " ret[${depth - 1}] - " + "".padEnd(2 * depth, ' ') + else -> " " + "".padEnd(2 * depth, ' ') } "$prefix$stmt" } - return " MD5(path)=${prettifiedPath.md5()}\n$prettifiedPath" + + fun definitelyFork() { + stateAnalyticsProperties.definitelyFork() } + + fun updateIsVisitedNew() { + stateAnalyticsProperties.updateIsVisitedNew() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ExecutionState + + if (stmt != other.stmt) return false + if (symbolicState != other.symbolicState) return false + if (executionStack != other.executionStack) return false + if (path != other.path) return false + if (visitedStatementsHashesToCountInPath != other.visitedStatementsHashesToCountInPath) return false + if (decisionPath != other.decisionPath) return false + if (edges != other.edges) return false + if (stmts != other.stmts) return false + if (pathLength != other.pathLength) return false + if (lastEdge != other.lastEdge) return false + if (lastMethod != other.lastMethod) return false + if (methodResult != other.methodResult) return false + if (exception != other.exception) return false + + return true + } + + private val hashCode by lazy { + Objects.hash( + stmt, symbolicState, executionStack, path, visitedStatementsHashesToCountInPath, decisionPath, + edges, stmts, pathLength, lastEdge, lastMethod, methodResult, exception + ) + } + + override fun hashCode(): Int = hashCode + + var reward by stateAnalyticsProperties::reward + val features by stateAnalyticsProperties::features + var executingTime by stateAnalyticsProperties::executingTime + val depth by stateAnalyticsProperties::depth + var visitedBeforeLastFork by stateAnalyticsProperties::visitedBeforeLastFork + var visitedAfterLastFork by stateAnalyticsProperties::visitedAfterLastFork + var stmtsSinceLastCovered by stateAnalyticsProperties::stmtsSinceLastCovered + val parent by stateAnalyticsProperties::parent } \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt index 0010195e18..d94d51b1b6 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Extensions.kt @@ -30,7 +30,6 @@ import org.utbot.engine.pc.mkLong import org.utbot.engine.pc.mkShort import org.utbot.engine.pc.mkString import org.utbot.engine.pc.toSort -import org.utbot.framework.UtSettings.checkNpeForFinalFields import org.utbot.framework.UtSettings.checkNpeInNestedMethods import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods import org.utbot.framework.plugin.api.FieldId @@ -46,6 +45,7 @@ import kotlin.reflect.jvm.javaConstructor import kotlin.reflect.jvm.javaMethod import kotlinx.collections.immutable.PersistentMap import kotlinx.collections.immutable.persistentHashMapOf +import org.utbot.engine.pc.UtSolverStatusUNDEFINED import soot.ArrayType import soot.PrimType import soot.RefLikeType @@ -382,11 +382,7 @@ fun arrayTypeUpdate(addr: UtAddrExpression, type: ArrayType) = fun SootField.shouldBeNotNull(): Boolean { require(type is RefLikeType) - if (hasNotNullAnnotation()) return true - - if (!checkNpeForFinalFields && isFinal) return true - - return false + return hasNotNullAnnotation() } /** @@ -429,21 +425,33 @@ val SootClass.isUtMock: Boolean val SootClass.isOverridden: Boolean get() = packageName.startsWith(UTBOT_OVERRIDE_PACKAGE_NAME) +val SootClass.isLibraryNonOverriddenClass: Boolean + get() = isLibraryClass && !isOverridden + /** * Returns a state from the list that has [UtSolverStatusSAT] status. * Inside it calls UtSolver.check if required. + * + * [processStatesWithUnknownStatus] is responsible for solver checks for states + * with unknown status. Note that this calculation might take a long time. */ -fun MutableList.pollUntilSat(): ExecutionState? { +fun MutableList.pollUntilSat(processStatesWithUnknownStatus: Boolean): ExecutionState? { while (!isEmpty()) { val state = removeLast() with(state.solver) { + if (lastStatus.statusKind == UtSolverStatusKind.UNSAT) return@with + if (lastStatus.statusKind == UtSolverStatusKind.SAT) return state - if (lastStatus.statusKind == UtSolverStatusKind.UNKNOWN) { - val result = check(respectSoft = true) + require(failedAssumptions.isEmpty()) { "There are failed requirements in the queue to execute concretely" } + + if (processStatesWithUnknownStatus) { + if (lastStatus == UtSolverStatusUNDEFINED) { + val result = check(respectSoft = true) - if (result.statusKind == UtSolverStatusKind.SAT) return state + if (result.statusKind == UtSolverStatusKind.SAT) return state + } } } } @@ -455,3 +463,23 @@ fun isOverriddenClass(type: RefType) = type.sootClass.isOverridden val SootMethod.isSynthetic: Boolean get() = soot.Modifier.isSynthetic(modifiers) + +/** + * Returns true if the [SootMethod]'s signature is equal to [UtMock.assume]'s signature, false otherwise. + */ +val SootMethod.isUtMockAssume + get() = signature == assumeMethod.signature + +/** + * Returns true if the [SootMethod]'s signature is equal to + * [UtMock.assumeOrExecuteConcretely]'s signature, false otherwise. + */ +val SootMethod.isUtMockAssumeOrExecuteConcretely + get() = signature == assumeOrExecuteConcretelyMethod.signature + +/** + * Returns true is the [SootMethod] is defined in a class from + * [UTBOT_OVERRIDE_PACKAGE_NAME] package and its name is `preconditionCheck`. + */ +val SootMethod.isPreconditionCheckMethod + get() = declaringClass.isOverridden && name == "preconditionCheck" diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt index 7485abe927..ff533b1085 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/InterProceduralUnitGraph.kt @@ -7,6 +7,7 @@ import soot.jimple.Stmt import soot.jimple.SwitchStmt import soot.jimple.internal.JReturnStmt import soot.jimple.internal.JReturnVoidStmt +import soot.jimple.internal.JThrowStmt import soot.toolkits.graph.ExceptionalUnitGraph private const val EXCEPTION_DECISION_SHIFT = 100000 @@ -22,14 +23,28 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { private var attachAllowed = true val graphs = mutableListOf(graph) private val statistics = mutableListOf() - private val methods = mutableListOf(graph.body.method) + // All the methods in the InterProceduralUnitGraph + private val methods: MutableSet = mutableSetOf(graph.body.method) + // Methods with registered edges + private val registeredMethods: MutableSet = mutableSetOf(graph.body.method) + + // We use this field for exceptional statements of the methods from the [registeredMethods] map. + // Points of interest are statements of the graph that must be covered during the exploration. + // Therefore, there is no need to put here statements from registered methods since + // we try to cover all of their edges. + private val methodToUncoveredThrowStatements: MutableMap> = mutableMapOf() private val unexceptionalSuccsCache = mutableMapOf>>() private val exceptionalSuccsCache = mutableMapOf>>() private val edgesCache = mutableMapOf>() val stmts: MutableSet = graph.stmts.toMutableSet() - val registeredEdges: MutableSet = graph.edges.toMutableSet() + private val registeredEdges: MutableSet = graph.edges.toMutableSet() + // this field is required for visualization + private val allExplicitEdges: MutableSet = graph.edges.toMutableSet() + + val allEdges: Set get() = allExplicitEdges + implicitEdges + /** * Used in [org.utbot.engine.selectors.nurs.InheritorsSelector] for a fast search of Virtual invoke successors. */ @@ -41,9 +56,11 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { private val unexceptionalSuccs: MutableMap> = graph.unexceptionalSuccs.toMutableMap() private val stmtToGraph: MutableMap = stmts.associateWithTo(mutableMapOf()) { graph } + private val edgeToGraph: MutableMap = graph.edges.associateWithTo(mutableMapOf()) { graph } - val registeredEdgesCount: MutableMap = graph.outgoingEdgesCount - val outgoingEdgesCount: MutableMap = graph.outgoingEdgesCount + private val registeredEdgesCount: MutableMap = graph.outgoingEdgesCount + private val allEdgesCount: MutableMap = graph.outgoingEdgesCount + private val outgoingEdgesCount: MutableMap = graph.outgoingEdgesCount fun method(stmt: Stmt): SootMethod = stmtToGraph[stmt]?.body?.method ?: error("$stmt not in graph.") @@ -79,42 +96,70 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { private val ExceptionalUnitGraph.outgoingEdgesCount: MutableMap get() = stmts.associateWithTo(mutableMapOf()) { succ(it).count() } + /** + * Returns a method that the [edge] is belongs to. If the [edge] does not + * have such method, null will be returned. + * + * Note: for example, implicit edges and edges produced by an invocation + * does not have a method they could belong to. + */ + private fun methodByEdge(edge: Edge): SootMethod? = edgeToGraph[edge]?.body?.method /** - * join new graph to stmt. - * @param registerEdges - if true than edges + * Joins a new [graph] to the [stmt]. Depending on the [registerEdges], edges of the [graph] + * might be either registered in the global graph or not. + * + * If they are registered, path selector tries to cover all edges from the [graph]. */ fun join(stmt: Stmt, graph: ExceptionalUnitGraph, registerEdges: Boolean) { attachAllowed = false + val invokeEdge = Edge(stmt, graph.head, CALL_DECISION_NUM) - val alreadyJoined = graph.body.method in methods + val method = graph.body.method + val alreadyJoined = method in methods + if (!alreadyJoined) { graphs += graph - methods += graph.body.method + methods += method traps += graph.traps exceptionalSuccs += graph.exceptionalSuccs unexceptionalSuccs += graph.unexceptionalSuccs - val joinedStmts = graph.stmts - stmts += joinedStmts - joinedStmts.forEach { stmtToGraph[it] = graph } + graph.edges.forEach { edgeToGraph[it] = graph } + + val joinedStmts = graph.stmts.onEach { stmtToGraph[it] = graph } outgoingEdgesCount += graph.outgoingEdgesCount + stmts += joinedStmts + + allExplicitEdges += graph.edges + allEdgesCount += graph.outgoingEdgesCount + allEdgesCount.computeIfPresent(stmt) { _, value -> value + 1 } + if (registerEdges) { + registeredMethods += method registeredEdgesCount += graph.outgoingEdgesCount registeredEdges += graph.edges registeredEdgesCount.computeIfPresent(stmt) { _, value -> value + 1 } } + + // it is important to have this method call after we register the given method + // because we find uncoveredPoints for unregistered methods only + method.uncoveredThrowStatements() } + invokeSuccessors.compute(stmt) { _, value -> value?.apply { add(graph.head) } ?: mutableSetOf(graph.head) } + registeredEdges += invokeEdge + allExplicitEdges += invokeEdge outgoingEdgesCount.computeIfPresent(stmt) { _, value -> value + 1 } + statistics.forEach { it.onJoin(stmt, graph, registerEdges) } @@ -129,10 +174,13 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { coveredOutgoingEdges.putIfAbsent(executionState.stmt, 0) for (edge in executionState.edges) { + markAsCoveredStmt(edge.src) + markAsCoveredStmt(edge.dst) + when (edge) { in implicitEdges -> coveredImplicitEdges += edge - !in registeredEdges -> coveredOutgoingEdges.putIfAbsent(edge.src, 0) - !in coveredEdges -> { + !in registeredEdges, !in coveredEdges-> { + coveredOutgoingEdges.putIfAbsent(edge.src, 0) coveredOutgoingEdges.compute(edge.src) { _, value -> (value ?: 0) + 1 } coveredEdges += edge } @@ -143,6 +191,43 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { } } + /** + * Returns uncovered throw statements for [this] method. + */ + private fun SootMethod.uncoveredThrowStatements(): Set { + val throwStatements = methodToUncoveredThrowStatements[this] + + if (throwStatements != null) { + return throwStatements + } + + // We don't have to additionally specify throw statements + // since for the registeredMethods we try to cover all of their edges. + if (this in registeredMethods) { + return emptySet() + } + + // We don't want to cover all the exceptions in not overridden library classes + if (declaringClass.isLibraryNonOverriddenClass) { + return emptySet() + } + + val uncoveredThrowStatements = methodToUncoveredThrowStatements.getOrPut(this) { mutableSetOf() } + + if (!canRetrieveBody()) { + return uncoveredThrowStatements + } + + uncoveredThrowStatements += jimpleBody().units.filterIsInstance() + + return uncoveredThrowStatements + } + + private fun markAsCoveredStmt(stmt: Stmt) { + val method = methodByStmt(stmt) + methodToUncoveredThrowStatements[method]?.remove(stmt) + } + /** * register new implicit edge, that is not represented in graph */ @@ -173,6 +258,7 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { /** * attach new statistics to graph before any modifying operations */ + @Suppress("UNREACHABLE_CODE") fun attach(statistics: TraverseGraphStatistics) { if (!attachAllowed) { throw error("Cannot attach new statistics. Graph have modified.") @@ -203,7 +289,10 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { * Statement is covered if all the outgoing edges are covered. */ fun isCovered(stmt: Stmt) = - stmt !in registeredEdgesCount || stmt in coveredOutgoingEdges && coveredOutgoingEdges[stmt]!! >= registeredEdgesCount[stmt]!! + stmt !in registeredEdgesCount || isCoveredIgnoringRegistration(stmt) + + fun isCoveredIgnoringRegistration(stmt: Stmt) = + stmt in coveredOutgoingEdges && coveredOutgoingEdges[stmt]!! >= allEdgesCount[stmt]!! /** * Edge is covered if we visited it in successfully completed execution at least once @@ -215,11 +304,34 @@ class InterProceduralUnitGraph(graph: ExceptionalUnitGraph) { (edge in coveredEdges || edge !in registeredEdges) } + /** + * If the [edge] does not have associated method, returns a result of [isCovered] call. + * Otherwise, there are several cases: + * * If the [edge] in [coveredEdges], returns true; + * * If the [edge] belongs to a method of some overridden class and there is + * uncovered throw statements in it, returns false; + * * In all other cases, returns whether the [edge] absent in [registeredEdges]. + */ + fun isCoveredWithAllThrowStatements(edge: Edge): Boolean { + val method = methodByEdge(edge) ?: return isCovered(edge) + + if (edge in coveredEdges) { + return true + } + + if (method.declaringClass.isOverridden && method.uncoveredThrowStatements().isNotEmpty()) { + return false + } + + return edge !in registeredEdges + } /** * @return true if stmt has more than one outgoing edge */ fun isFork(stmt: Stmt): Boolean = (outgoingEdgesCount[stmt] ?: 0) > 1 + + private fun methodByStmt(stmt: Stmt) = stmtToGraph.getValue(stmt).body.method } private fun ExceptionalUnitGraph.succ(stmt: Stmt): Sequence = exceptionalSucc(stmt) + unexceptionalSucc(stmt) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt index 0ca5359db9..cf77433848 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Memory.kt @@ -137,7 +137,17 @@ data class Memory( // TODO: split purely symbolic memory and information about s UtFalse, UtArraySort(UtAddrSort, UtBoolSort) ), - private val instanceFieldReadOperations: PersistentSet = persistentHashSetOf() + private val instanceFieldReadOperations: PersistentSet = persistentHashSetOf(), + + /** + * Storage for addresses that we speculatively consider non-nullable (e.g., final fields of system classes). + * See [org.utbot.engine.UtBotSymbolicEngine.createFieldOrMock] for usage, + * and [docs/SpeculativeFieldNonNullability.md] for details. + */ + private val speculativelyNotNullAddresses: UtArrayExpressionBase = UtConstArrayExpression( + UtFalse, + UtArraySort(UtAddrSort, UtBoolSort) + ) ) { val chunkIds: Set get() = initial.keys @@ -167,6 +177,11 @@ data class Memory( // TODO: split purely symbolic memory and information about s */ fun isTouched(addr: UtAddrExpression): UtArraySelectExpression = touchedAddresses.select(addr) + /** + * Returns symbolic information about whether [addr] corresponds to a final field known to be not null. + */ + fun isSpeculativelyNotNull(addr: UtAddrExpression): UtArraySelectExpression = speculativelyNotNullAddresses.select(addr) + /** * @return ImmutableCollection of the initial values for all the arrays we touched during the execution */ @@ -260,6 +275,10 @@ data class Memory( // TODO: split purely symbolic memory and information about s acc.store(addr, UtTrue) } + val updSpeculativelyNotNullAddresses = update.speculativelyNotNullAddresses.fold(speculativelyNotNullAddresses) { acc, addr -> + acc.store(addr, UtTrue) + } + return this.copy( initial = updInitial, current = updCurrent, @@ -275,7 +294,8 @@ data class Memory( // TODO: split purely symbolic memory and information about s updates = updates + update, visitedValues = updVisitedValues, touchedAddresses = updTouchedAddresses, - instanceFieldReadOperations = instanceFieldReadOperations.addAll(update.instanceFieldReads) + instanceFieldReadOperations = instanceFieldReadOperations.addAll(update.instanceFieldReads), + speculativelyNotNullAddresses = updSpeculativelyNotNullAddresses ) } @@ -351,6 +371,11 @@ class TypeRegistry { private val typeToInheritorsTypes = mutableMapOf>() private val typeToAncestorsTypes = mutableMapOf>() + /** + * Contains types storages for type parameters of object by its address. + */ + private val genericTypeStorageByAddr = mutableMapOf>() + // A BiMap containing bijection from every type to an address of the object // presenting its classRef and vise versa private val classRefBiMap = HashBiMap.create() @@ -431,7 +456,6 @@ class TypeRegistry { private val genericAddrToNumDimensionsArrays = mutableMapOf() - @Suppress("unused") private fun genericAddrToNumDimensions(i: Int) = genericAddrToNumDimensionsArrays.getOrPut(i) { mkArrayConst( "genericAddrToNumDimensions_$i", @@ -562,6 +586,8 @@ class TypeRegistry { */ fun symNumDimensions(addr: UtAddrExpression) = addrToNumDimensions.select(addr) + fun genericNumDimensions(addr: UtAddrExpression, i: Int) = genericAddrToNumDimensions(i).select(addr) + /** * Returns a constraint stating that number of dimensions for the given address is zero */ @@ -700,7 +726,11 @@ class TypeRegistry { firstAddr: UtAddrExpression, secondAddr: UtAddrExpression, vararg indexInjection: Pair - ) = UtEqGenericTypeParametersExpression(firstAddr, secondAddr, mapOf(*indexInjection)) + ): UtEqGenericTypeParametersExpression { + setParameterTypeStoragesEquality(firstAddr, secondAddr, indexInjection) + + return UtEqGenericTypeParametersExpression(firstAddr, secondAddr, mapOf(*indexInjection)) + } /** * returns constraint representing that type parameters of an object with address [firstAddr] are equal to @@ -711,7 +741,11 @@ class TypeRegistry { firstAddr: UtAddrExpression, secondAddr: UtAddrExpression, parameterSize: Int - ) = UtEqGenericTypeParametersExpression(firstAddr, secondAddr, (0 until parameterSize).associateWith { it }) + ) : UtEqGenericTypeParametersExpression { + val injections = Array(parameterSize) { it to it } + + return eqGenericTypeParametersConstraint(firstAddr, secondAddr, *injections) + } /** * returns constraint representing that the first type parameter of an object with address [firstAddr] are equal to @@ -719,7 +753,46 @@ class TypeRegistry { * @see UtEqGenericTypeParametersExpression */ fun eqGenericSingleTypeParameterConstraint(firstAddr: UtAddrExpression, secondAddr: UtAddrExpression) = - UtEqGenericTypeParametersExpression(firstAddr, secondAddr, mapOf(0 to 0)) + eqGenericTypeParametersConstraint(firstAddr, secondAddr, 0 to 0) + + /** + * Associates provided [typeStorages] with an object with the provided [addr]. + */ + fun saveObjectParameterTypeStorages(addr: UtAddrExpression, typeStorages: List) { + genericTypeStorageByAddr += addr to typeStorages + } + + /** + * Retrieves parameter type storages of an object with the given [addr] if present, or null otherwise. + */ + fun getTypeStoragesForObjectTypeParameters(addr: UtAddrExpression): List? = genericTypeStorageByAddr[addr] + + /** + * Set types storages for [firstAddr]'s type parameters equal to type storages for [secondAddr]'s type parameters + * according to provided types injection represented by [indexInjection]. + */ + private fun setParameterTypeStoragesEquality( + firstAddr: UtAddrExpression, + secondAddr: UtAddrExpression, + indexInjection: Array> + ) { + val existingGenericTypes = genericTypeStorageByAddr[secondAddr] ?: return + + val currentGenericTypes = mutableMapOf() + + indexInjection.forEach { (from, to) -> + require(from >= 0 && from < existingGenericTypes.size) { + "Type injection is out of bounds: should be in [0; ${existingGenericTypes.size}) but is $from" + } + + currentGenericTypes[to] = existingGenericTypes[from] + } + + genericTypeStorageByAddr[firstAddr] = currentGenericTypes + .entries + .sortedBy { it.key } + .mapTo(mutableListOf()) { it.value } + } /** * Returns constraint representing that an object with address [addr] has the same type as the type parameter @@ -902,7 +975,8 @@ data class MemoryUpdate( val visitedValues: PersistentList = persistentListOf(), val touchedAddresses: PersistentList = persistentListOf(), val classIdToClearStatics: ClassId? = null, - val instanceFieldReads: PersistentSet = persistentHashSetOf() + val instanceFieldReads: PersistentSet = persistentHashSetOf(), + val speculativelyNotNullAddresses: PersistentList = persistentListOf() ) { operator fun plus(other: MemoryUpdate) = this.copy( @@ -919,7 +993,8 @@ data class MemoryUpdate( visitedValues = visitedValues.addAll(other.visitedValues), touchedAddresses = touchedAddresses.addAll(other.touchedAddresses), classIdToClearStatics = other.classIdToClearStatics, - instanceFieldReads = instanceFieldReads.addAll(other.instanceFieldReads) + instanceFieldReads = instanceFieldReads.addAll(other.instanceFieldReads), + speculativelyNotNullAddresses = speculativelyNotNullAddresses.addAll(other.speculativelyNotNullAddresses), ) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt index 4d9d12a03f..913990c39e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Mocks.kt @@ -18,6 +18,7 @@ import java.util.concurrent.atomic.AtomicInteger import kotlin.reflect.KFunction2 import kotlin.reflect.KFunction5 import kotlinx.collections.immutable.persistentListOf +import org.utbot.engine.util.mockListeners.MockListenerController import soot.BooleanType import soot.RefType import soot.Scene @@ -133,7 +134,8 @@ class Mocker( private val strategy: MockStrategy, private val classUnderTest: ClassId, private val hierarchy: Hierarchy, - chosenClassesToMockAlways: Set + chosenClassesToMockAlways: Set, + internal val mockListenerController: MockListenerController? = null, ) { /** * Creates mocked instance of the [type] using mock info if it should be mocked by the mocker, @@ -164,21 +166,35 @@ class Mocker( * For others, if mock is not a new instance mock, asks mock strategy for decision. */ fun shouldMock( + type: RefType, + mockInfo: UtMockInfo, + ): Boolean = checkIfShouldMock(type, mockInfo).also { if (it) mockListenerController?.onShouldMock(strategy) } + + private fun checkIfShouldMock( type: RefType, mockInfo: UtMockInfo ): Boolean { if (isUtMockAssume(mockInfo)) return false // never mock UtMock.assume invocation + if (isUtMockAssumeOrExecuteConcretely(mockInfo)) return false // never mock UtMock.assumeOrExecuteConcretely invocation if (isOverriddenClass(type)) return false // never mock overriden classes if (isMakeSymbolic(mockInfo)) return true // support for makeSymbolic if (type.sootClass.isArtificialEntity) return false // never mock artificial types, i.e. Maps$lambda_computeValue_1__7 - //TODO: SAT-1496 verify that using SootClass here is correct if (!isEngineClass(type) && (type.sootClass.isInnerClass || type.sootClass.isLocal || type.sootClass.isAnonymous)) return false // there is no reason (and maybe no possibility) to mock such classes if (!isEngineClass(type) && type.sootClass.isPrivate) return false // could not mock private classes (even if it is in mock always list) if (mockAlways(type)) return true // always mock randoms and loggers - if (type.sootClass.isArtificialEntity) return false // never mock artificial types, i.e. Maps$lambda_computeValue_1__7 if (mockInfo is UtFieldMockInfo) { + val declaringClass = mockInfo.fieldId.declaringClass + val sootDeclaringClass = Scene.v().getSootClass(declaringClass.name) + + if (sootDeclaringClass.isArtificialEntity || sootDeclaringClass.isOverridden) { + // Cannot load Java class for artificial classes, see BaseStreamExample::minExample for an example. + // Wrapper classes that override system classes ([org.utbot.engine.overrides] package) are also + // unavailable to the [UtContext] class loader used by the plugin. + return false + } + return when { - mockInfo.fieldId.declaringClass.packageName.startsWith("java.lang") -> false + declaringClass.packageName.startsWith("java.lang") -> false !mockInfo.fieldId.type.isRefType -> false // mocks are allowed for ref fields only else -> return strategy.eligibleToMock(mockInfo.fieldId.type, classUnderTest) // if we have a field with Integer type, we should not mock it } @@ -197,6 +213,9 @@ class Mocker( private fun isUtMockAssume(mockInfo: UtMockInfo) = mockInfo is UtStaticMethodMockInfo && mockInfo.methodId.signature == assumeBytecodeSignature + private fun isUtMockAssumeOrExecuteConcretely(mockInfo: UtMockInfo) = + mockInfo is UtStaticMethodMockInfo && mockInfo.methodId.signature == assumeOrExecuteConcretelyBytecodeSignature + private fun isEngineClass(type: RefType) = type.className in engineClasses private fun mockAlways(type: RefType) = type.className in classesToMockAlways @@ -304,6 +323,9 @@ internal val nonNullableMakeSymbolic: SootMethod internal val assumeMethod: SootMethod get() = utMockClass.getMethod(ASSUME_NAME, listOf(BooleanType.v())) +internal val assumeOrExecuteConcretelyMethod: SootMethod + get() = utMockClass.getMethod(ASSUME_OR_EXECUTE_CONCRETELY_NAME, listOf(BooleanType.v())) + val makeSymbolicBytecodeSignature: String get() = makeSymbolicMethod.executableId.signature @@ -313,6 +335,9 @@ val nonNullableMakeSymbolicBytecodeSignature: String val assumeBytecodeSignature: String get() = assumeMethod.executableId.signature +val assumeOrExecuteConcretelyBytecodeSignature: String + get() = assumeOrExecuteConcretelyMethod.executableId.signature + internal val UTBOT_OVERRIDE_PACKAGE_NAME = UtOverrideMock::class.java.packageName private val arraycopyMethod : KFunction5, Int, Array, Int, Int, Unit> = UtArrayMock::arraycopy @@ -331,3 +356,4 @@ internal val utLogicMockIteMethodName = UtLogicMock::ite.name private const val MAKE_SYMBOLIC_NAME = "makeSymbolic" private const val ASSUME_NAME = "assume" +private const val ASSUME_OR_EXECUTE_CONCRETELY_NAME = "assumeOrExecuteConcretely" diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt index ccbf39e1a1..3a162ed092 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ObjectWrappers.kt @@ -8,6 +8,7 @@ import org.utbot.engine.UtOptionalClass.UT_OPTIONAL import org.utbot.engine.UtOptionalClass.UT_OPTIONAL_DOUBLE import org.utbot.engine.UtOptionalClass.UT_OPTIONAL_INT import org.utbot.engine.UtOptionalClass.UT_OPTIONAL_LONG +import org.utbot.engine.UtStreamClass.UT_STREAM import org.utbot.engine.overrides.collections.AssociativeArray import org.utbot.engine.overrides.collections.RangeModifiableUnlimitedArray import org.utbot.engine.overrides.collections.UtHashMap @@ -27,16 +28,16 @@ import org.utbot.framework.plugin.api.util.constructorId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.framework.util.nextModelName +import soot.RefType +import soot.Scene +import soot.SootClass +import soot.SootMethod import java.util.Optional import java.util.OptionalDouble import java.util.OptionalInt import java.util.OptionalLong import java.util.concurrent.CopyOnWriteArrayList import kotlin.reflect.KClass -import soot.RefType -import soot.Scene -import soot.SootClass -import soot.SootMethod typealias TypeToBeWrapped = RefType typealias WrapperType = RefType @@ -81,6 +82,10 @@ val classToWrapper: MutableMap = putSootClass(java.util.AbstractMap::class, UtHashMap::class) putSootClass(java.util.LinkedHashMap::class, UtHashMap::class) putSootClass(java.util.HashMap::class, UtHashMap::class) + + putSootClass(java.util.stream.BaseStream::class, UT_STREAM.className) + putSootClass(java.util.stream.Stream::class, UT_STREAM.className) + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 } /** @@ -176,7 +181,12 @@ private val wrappers = mapOf( wrap(java.util.Map::class) { _, addr -> objectValue(LINKED_HASH_MAP_TYPE, addr, MapWrapper()) }, wrap(java.util.AbstractMap::class) { _, addr -> objectValue(LINKED_HASH_MAP_TYPE, addr, MapWrapper()) }, wrap(java.util.LinkedHashMap::class) { _, addr -> objectValue(LINKED_HASH_MAP_TYPE, addr, MapWrapper()) }, - wrap(java.util.HashMap::class) { _, addr -> objectValue(HASH_MAP_TYPE, addr, MapWrapper()) } + wrap(java.util.HashMap::class) { _, addr -> objectValue(HASH_MAP_TYPE, addr, MapWrapper()) }, + + // stream wrappers + wrap(java.util.stream.BaseStream::class) { _, addr -> objectValue(STREAM_TYPE, addr, CommonStreamWrapper()) }, + wrap(java.util.stream.Stream::class) { _, addr -> objectValue(STREAM_TYPE, addr, CommonStreamWrapper()) }, + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 ).also { // check every `wrapped` class has a corresponding value in [classToWrapper] it.keys.all { key -> @@ -187,7 +197,7 @@ private val wrappers = mapOf( private fun wrap(kClass: KClass<*>, implementation: (RefType, UtAddrExpression) -> ObjectValue) = kClass.id to implementation -internal fun wrapper(type: RefType, addr: UtAddrExpression) = +internal fun wrapper(type: RefType, addr: UtAddrExpression): ObjectValue? = wrappers[type.id]?.invoke(type, addr) interface WrapperInterface { diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt index f806f15305..19348fc6f9 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/OptionalWrapper.kt @@ -29,7 +29,7 @@ import soot.Scene import soot.SootMethod /** - * Auxiliary enum class for specifying an implementation for [ListWrapper], that it will use. + * Auxiliary enum class for specifying an implementation for [OptionalWrapper], that it will use. */ enum class UtOptionalClass { UT_OPTIONAL, @@ -81,6 +81,7 @@ class OptionalWrapper(private val utOptionalClass: UtOptionalClass) : BaseOverri } } + return null } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt index a67d675cb0..4fca4b478d 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Resolver.kt @@ -25,6 +25,7 @@ import org.utbot.engine.pc.mkLong import org.utbot.engine.pc.mkShort import org.utbot.engine.pc.select import org.utbot.engine.pc.store +import org.utbot.engine.util.statics.concrete.isEnumValuesFieldName import org.utbot.engine.symbolic.asHardConstraint import org.utbot.engine.z3.intValue import org.utbot.engine.z3.value @@ -82,6 +83,7 @@ import soot.PrimType import soot.RefType import soot.Scene import soot.ShortType +import soot.SootClass import soot.SootField import soot.Type import soot.VoidType @@ -1068,7 +1070,7 @@ fun UtBotSymbolicEngine.toMethodResult(value: Any?, sootType: Type): MethodResul arrayToMethodResult(value.size, elementType) { if (value[it] == null) return@arrayToMethodResult nullObjectAddr - val addr = UtAddrExpression(mkBVConst("staticVariable${value.hashCode()}", UtInt32Sort)) + val addr = UtAddrExpression(mkBVConst("staticVariable${value.hashCode()}_$it", UtInt32Sort)) val createdElement = if (elementType is RefType) { val className = value[it]!!.javaClass.id.name @@ -1141,6 +1143,30 @@ private fun UtBotSymbolicEngine.arrayToMethodResult( ) } +fun UtBotSymbolicEngine.constructEnumStaticFieldResult( + fieldName: String, + fieldType: Type, + declaringClass: SootClass, + enumConstantSymbolicResultsByName: Map, + staticFieldConcreteValue: Any?, + enumConstantSymbolicValues: List +): MethodResult = + if (isEnumValuesFieldName(fieldName)) { + // special case to fill $VALUES array with already created symbolic values for enum constants runtime values + arrayToMethodResult(enumConstantSymbolicValues.size, declaringClass.type) { i -> + enumConstantSymbolicValues[i].addr + } + } else { + if (fieldName in enumConstantSymbolicResultsByName) { + // it is field to store enum constant so we use already created symbolic value from runtime enum constant + enumConstantSymbolicResultsByName.getValue(fieldName) + } else { + // otherwise, it is a common static field, + // and we have to create new symbolic value for it using its concrete value + toMethodResult(staticFieldConcreteValue, fieldType) + } + } + private val Type.unsigned: Boolean get() = this is CharType diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt new file mode 100644 index 0000000000..e3cfe68a1b --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/StreamWrappers.kt @@ -0,0 +1,117 @@ +package org.utbot.engine + +import org.utbot.engine.overrides.stream.UtStream +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.MethodId +import org.utbot.framework.plugin.api.UtArrayModel +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.classId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.methodId +import org.utbot.framework.plugin.api.util.objectArrayClassId +import org.utbot.framework.plugin.api.util.objectClassId +import org.utbot.framework.util.nextModelName +import soot.RefType +import soot.Scene + +/** + * Auxiliary enum class for specifying an implementation for [CommonStreamWrapper], that it will use. + */ +// TODO introduce a base interface for all such enums after https://github.com/UnitTestBot/UTBotJava/issues/146? +enum class UtStreamClass { + UT_STREAM; + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 +// UT_STREAM_INT, +// UT_STREAM_LONG, +// UT_STREAM_DOUBLE; + + val className: String + get() = when (this) { + UT_STREAM -> UtStream::class.java.canonicalName + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 +// UT_STREAM_INT -> UtStreamInt::class.java.canonicalName +// UT_STREAM_LONG -> UtStreamLong::class.java.canonicalName +// UT_STREAM_DOUBLE -> UtStreamDouble::class.java.canonicalName + } + + val elementClassId: ClassId + get() = when (this) { + UT_STREAM -> objectClassId + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 +// UT_STREAM_INT -> intClassId +// UT_STREAM_LONG -> longClassId +// UT_STREAM_DOUBLE -> doubleClassId + } + + val overriddenStreamClassId: ClassId + get() = when (this) { + UT_STREAM -> java.util.stream.Stream::class.java.id + // TODO primitive streams https://github.com/UnitTestBot/UTBotJava/issues/146 + } +} + +abstract class StreamWrapper( + private val utStreamClass: UtStreamClass +) : BaseGenericStorageBasedContainerWrapper(utStreamClass.className) { + override fun value(resolver: Resolver, wrapper: ObjectValue): UtAssembleModel = resolver.run { + val addr = holder.concreteAddr(wrapper.addr) + val modelName = nextModelName(baseModelName) + val parametersArrayModel = resolveElementsAsArrayModel(wrapper) + + val instantiationChain = mutableListOf() + val modificationsChain = emptyList() + + UtAssembleModel(addr, utStreamClass.overriddenStreamClassId, modelName, instantiationChain, modificationsChain) + .apply { + val (builder, params) = if (parametersArrayModel == null || parametersArrayModel.length == 0) { + streamEmptyMethodId to emptyList() + } else { + streamOfMethodId to listOf(parametersArrayModel) + } + + instantiationChain += UtExecutableCallModel( + instance = null, + executable = builder, + params = params, + returnValue = this + ) + } + } + + override fun chooseClassIdWithConstructor(classId: ClassId): ClassId = error("No constructor for Stream") + + override val modificationMethodId: MethodId + get() = error("No modification method for Stream") + + private fun Resolver.resolveElementsAsArrayModel(wrapper: ObjectValue): UtArrayModel? { + val elementDataFieldId = FieldId(overriddenClass.type.classId, "elementData") + + return collectFieldModels(wrapper.addr, overriddenClass.type)[elementDataFieldId] as? UtArrayModel + } + + private val streamOfMethodId: MethodId = methodId( + classId = utStreamClass.overriddenStreamClassId, + name = "of", + returnType = utStreamClass.overriddenStreamClassId, + arguments = arrayOf(objectArrayClassId) // vararg + ) + + private val streamEmptyMethodId: MethodId = methodId( + classId = utStreamClass.overriddenStreamClassId, + name = "empty", + returnType = utStreamClass.overriddenStreamClassId, + arguments = emptyArray() + ) +} + +class CommonStreamWrapper : StreamWrapper(UtStreamClass.UT_STREAM) { + override val baseModelName: String = "stream" + + companion object { + internal val utStreamType: RefType + get() = Scene.v().getSootClass(UtStream::class.java.canonicalName).type + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt index 80610efa1a..72695c927f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/Strings.kt @@ -72,9 +72,9 @@ class StringWrapper : BaseOverriddenWrapper(utStringClass.name) { method: SootMethod, parameters: List ): List? { - when (method.subSignature) { + return when (method.subSignature) { toStringMethodSignature -> { - return listOf(MethodResult(wrapper.copy(typeStorage = TypeStorage(method.returnType)))) + listOf(MethodResult(wrapper.copy(typeStorage = TypeStorage(method.returnType)))) } matchesMethodSignature -> { val arg = parameters[0] as ObjectValue @@ -82,7 +82,8 @@ class StringWrapper : BaseOverriddenWrapper(utStringClass.name) { if (!matchingLengthExpr.isConcrete) return null - val matchingValueExpr = selectArrayExpressionFromMemory(getValueArray(arg.addr)).accept(RewritingVisitor()) + val matchingValueExpr = + selectArrayExpressionFromMemory(getValueArray(arg.addr)).accept(RewritingVisitor()) val matchingLength = matchingLengthExpr.toConcrete() as Int val matchingValue = CharArray(matchingLength) @@ -124,7 +125,11 @@ class StringWrapper : BaseOverriddenWrapper(utStringClass.name) { val inBoundsCondition = mkAnd(Le(0.toPrimitiveValue(), index), Lt(index, lengthExpr.toIntValue())) val failMethodResult = MethodResult( - explicitThrown(StringIndexOutOfBoundsException(), findNewAddr(), isInNestedMethod()), + explicitThrown( + StringIndexOutOfBoundsException(), + findNewAddr(), + isInNestedMethod() + ), hardConstraints = mkNot(inBoundsCondition).asHardConstraint() ) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt index 054bf26cde..95565eb600 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/TypeResolver.kt @@ -18,7 +18,6 @@ import soot.Scene import soot.SootField import soot.Type import soot.VoidType -import sun.reflect.Reflection class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy: Hierarchy) { @@ -111,7 +110,7 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy fun constructTypeStorage(type: Type, possibleTypes: Collection): TypeStorage { val concretePossibleTypes = possibleTypes .map { (if (it is ArrayType) it.baseType else it) to it.numDimensions } - .filterNot { (baseType, _) -> baseType is RefType && baseType.sootClass.isInappropriate } + .filterNot { (baseType, numDimensions) -> isInappropriateOrArrayOfMocksOrLocals(numDimensions, baseType) } .mapTo(mutableSetOf()) { (baseType, numDimensions) -> if (numDimensions == 0) baseType else baseType.makeArrayType(numDimensions) } @@ -119,6 +118,26 @@ class TypeResolver(private val typeRegistry: TypeRegistry, private val hierarchy return TypeStorage(type, concretePossibleTypes).filterInappropriateClassesForCodeGeneration() } + private fun isInappropriateOrArrayOfMocksOrLocals(numDimensions: Int, baseType: Type?): Boolean { + if (baseType !is RefType) { + return false + } + + val baseSootClass = baseType.sootClass + + if (numDimensions == 0 && baseSootClass.isInappropriate) { + // interface, abstract class, or local, or mock could not be constructed + return true + } + + if (numDimensions > 0 && (baseSootClass.isLocal || baseSootClass.findMockAnnotationOrNull != null)) { + // array of mocks or locals could not be constructed, but array of interfaces or abstract classes could be + return true + } + + return false + } + /** * Constructs a [TypeStorage] instance for given [type]. * Depending on [useConcreteType] it will or will not contain type's inheritors. diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt index 12c4ef9bd5..053e1228f4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/UtBotSymbolicEngine.kt @@ -1,5 +1,26 @@ package org.utbot.engine +import kotlinx.collections.immutable.persistentHashMapOf +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentMap +import kotlinx.collections.immutable.toPersistentSet +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Job +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.isActive +import kotlinx.coroutines.job +import kotlinx.coroutines.yield +import mu.KotlinLogging +import org.utbot.analytics.EngineAnalyticsContext +import org.utbot.analytics.FeatureProcessor import org.utbot.analytics.Predictors import org.utbot.common.WorkaroundReason.HACK import org.utbot.common.WorkaroundReason.REMOVE_ANONYMOUS_CLASSES @@ -13,6 +34,7 @@ import org.utbot.engine.MockStrategy.NO_MOCKS import org.utbot.engine.overrides.UtArrayMock import org.utbot.engine.overrides.UtLogicMock import org.utbot.engine.overrides.UtOverrideMock +import org.utbot.engine.pc.NotBoolExpression import org.utbot.engine.pc.UtAddNoOverflowExpression import org.utbot.engine.pc.UtAddrExpression import org.utbot.engine.pc.UtAndBoolExpression @@ -38,7 +60,6 @@ import org.utbot.engine.pc.UtIteExpression import org.utbot.engine.pc.UtLongSort import org.utbot.engine.pc.UtMkTermArrayExpression import org.utbot.engine.pc.UtNegExpression -import org.utbot.engine.pc.NotBoolExpression import org.utbot.engine.pc.UtOrBoolExpression import org.utbot.engine.pc.UtPrimitiveSort import org.utbot.engine.pc.UtShortSort @@ -56,6 +77,7 @@ import org.utbot.engine.pc.mkBVConst import org.utbot.engine.pc.mkBoolConst import org.utbot.engine.pc.mkChar import org.utbot.engine.pc.mkEq +import org.utbot.engine.pc.mkFalse import org.utbot.engine.pc.mkFpConst import org.utbot.engine.pc.mkInt import org.utbot.engine.pc.mkNot @@ -63,116 +85,101 @@ import org.utbot.engine.pc.mkOr import org.utbot.engine.pc.select import org.utbot.engine.pc.store import org.utbot.engine.selectors.PathSelector +import org.utbot.engine.selectors.StrategyOption import org.utbot.engine.selectors.coveredNewSelector +import org.utbot.engine.selectors.cpInstSelector +import org.utbot.engine.selectors.forkDepthSelector import org.utbot.engine.selectors.inheritorsSelector +import org.utbot.engine.selectors.nnRewardGuidedSelector import org.utbot.engine.selectors.nurs.NonUniformRandomSearch import org.utbot.engine.selectors.pollUntilFastSAT +import org.utbot.engine.selectors.randomPathSelector +import org.utbot.engine.selectors.randomSelector import org.utbot.engine.selectors.strategies.GraphViz +import org.utbot.engine.selectors.subpathGuidedSelector import org.utbot.engine.symbolic.HardConstraint import org.utbot.engine.symbolic.SoftConstraint +import org.utbot.engine.symbolic.Assumption import org.utbot.engine.symbolic.SymbolicState import org.utbot.engine.symbolic.SymbolicStateUpdate import org.utbot.engine.symbolic.asHardConstraint import org.utbot.engine.symbolic.asSoftConstraint +import org.utbot.engine.symbolic.asAssumption import org.utbot.engine.symbolic.asUpdate +import org.utbot.engine.util.mockListeners.MockListener +import org.utbot.engine.util.mockListeners.MockListenerController +import org.utbot.engine.util.statics.concrete.associateEnumSootFieldsWithConcreteValues +import org.utbot.engine.util.statics.concrete.isEnumAffectingExternalStatics +import org.utbot.engine.util.statics.concrete.isEnumValuesFieldName +import org.utbot.engine.util.statics.concrete.makeEnumNonStaticFieldsUpdates +import org.utbot.engine.util.statics.concrete.makeEnumStaticFieldsUpdates +import org.utbot.engine.util.statics.concrete.makeSymbolicValuesFromEnumConcreteValues import org.utbot.framework.PathSelectorType import org.utbot.framework.UtSettings +import org.utbot.framework.UtSettings.checkNpeForFinalFields import org.utbot.framework.UtSettings.checkSolverTimeoutMillis +import org.utbot.framework.UtSettings.enableFeatureProcess import org.utbot.framework.UtSettings.pathSelectorStepsLimit import org.utbot.framework.UtSettings.pathSelectorType import org.utbot.framework.UtSettings.preferredCexOption import org.utbot.framework.UtSettings.substituteStaticsWithSymbolicVariable import org.utbot.framework.UtSettings.useDebugVisualization +import org.utbot.framework.UtSettings.processUnknownStatesDuringConcreteExecution import org.utbot.framework.concrete.UtConcreteExecutionData import org.utbot.framework.concrete.UtConcreteExecutionResult import org.utbot.framework.concrete.UtExecutionInstrumentation -import org.utbot.framework.concrete.UtModelConstructor import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConcreteExecutionFailureException import org.utbot.framework.plugin.api.EnvironmentModels import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.Instruction import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.MissingState import org.utbot.framework.plugin.api.Step -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.UtConcreteExecutionFailure import org.utbot.framework.plugin.api.UtError -import org.utbot.framework.plugin.api.UtExecutableCallModel import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtInstrumentation import org.utbot.framework.plugin.api.UtMethod -import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtNullModel import org.utbot.framework.plugin.api.UtOverflowFailure import org.utbot.framework.plugin.api.UtResult -import org.utbot.framework.plugin.api.UtStatementModel import org.utbot.framework.plugin.api.classId import org.utbot.framework.plugin.api.graph import org.utbot.framework.plugin.api.id import org.utbot.framework.plugin.api.onSuccess -import org.utbot.framework.plugin.api.util.booleanClassId -import org.utbot.framework.plugin.api.util.byteClassId -import org.utbot.framework.plugin.api.util.charClassId -import org.utbot.framework.plugin.api.util.defaultValueModel import org.utbot.framework.plugin.api.util.executableId -import org.utbot.framework.plugin.api.util.floatClassId import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.intClassId -import org.utbot.framework.plugin.api.util.isArray -import org.utbot.framework.plugin.api.util.isIterable -import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.jClass -import org.utbot.framework.plugin.api.util.kClass -import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.signature import org.utbot.framework.plugin.api.util.utContext import org.utbot.framework.util.description import org.utbot.framework.util.executableId -import org.utbot.fuzzer.FuzzedConcreteValue import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.FallbackModelProvider +import org.utbot.fuzzer.collectConstantsForFuzzer import org.utbot.fuzzer.defaultModelProviders import org.utbot.fuzzer.fuzz +import org.utbot.fuzzer.names.MethodBasedNameSuggester +import org.utbot.fuzzer.names.ModelBasedNameSuggester import org.utbot.instrumentation.ConcreteExecutor -import java.lang.reflect.Method import java.lang.reflect.ParameterizedType -import java.util.BitSet -import java.util.IdentityHashMap -import java.util.TreeSet import kotlin.collections.plus import kotlin.collections.plusAssign import kotlin.math.max import kotlin.math.min -import kotlin.reflect.KClass import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.valueParameters import kotlin.reflect.jvm.javaType -import kotlinx.collections.immutable.persistentHashMapOf -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.persistentSetOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.collections.immutable.toPersistentSet -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Job -import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.isActive -import kotlinx.coroutines.yield -import mu.KotlinLogging +import kotlin.system.measureTimeMillis import soot.ArrayType -import soot.Body import soot.BooleanType import soot.ByteType import soot.CharType import soot.DoubleType import soot.FloatType import soot.IntType -import soot.Local import soot.LongType import soot.PrimType import soot.RefLikeType @@ -196,7 +203,6 @@ import soot.jimple.Expr import soot.jimple.FieldRef import soot.jimple.FloatConstant import soot.jimple.IdentityRef -import soot.jimple.InstanceFieldRef import soot.jimple.IntConstant import soot.jimple.InvokeExpr import soot.jimple.LongConstant @@ -250,12 +256,14 @@ import soot.jimple.internal.JTableSwitchStmt import soot.jimple.internal.JThrowStmt import soot.jimple.internal.JVirtualInvokeExpr import soot.jimple.internal.JimpleLocal +import soot.tagkit.ParamNamesTag import soot.toolkits.graph.ExceptionalUnitGraph import sun.reflect.Reflection import sun.reflect.generics.reflectiveObjects.GenericArrayTypeImpl import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl import sun.reflect.generics.reflectiveObjects.TypeVariableImpl import sun.reflect.generics.reflectiveObjects.WildcardTypeImpl +import java.lang.reflect.Method private val logger = KotlinLogging.logger {} val pathLogger = KotlinLogging.logger(logger.name + ".path") @@ -284,7 +292,24 @@ private fun pathSelector(graph: InterProceduralUnitGraph, typeRegistry: TypeRegi PathSelectorType.INHERITORS_SELECTOR -> inheritorsSelector(graph, typeRegistry) { withStepsLimit(pathSelectorStepsLimit) } - else -> error("Unknown type") + PathSelectorType.SUBPATH_GUIDED_SELECTOR -> subpathGuidedSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } + PathSelectorType.CPI_SELECTOR -> cpInstSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } + PathSelectorType.FORK_DEPTH_SELECTOR -> forkDepthSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } + PathSelectorType.NN_REWARD_GUIDED_SELECTOR -> nnRewardGuidedSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } + PathSelectorType.RANDOM_SELECTOR -> randomSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } + PathSelectorType.RANDOM_PATH_SELECTOR -> randomPathSelector(graph, StrategyOption.DISTANCE) { + withStepsLimit(pathSelectorStepsLimit) + } } class UtBotSymbolicEngine( @@ -299,6 +324,7 @@ class UtBotSymbolicEngine( ) : UtContextInitializer() { private val methodUnderAnalysisStmts: Set = graph.stmts.toSet() + private val visitedStmts: MutableSet = mutableSetOf() private val globalGraph = InterProceduralUnitGraph(graph) internal val typeRegistry: TypeRegistry = TypeRegistry() private val pathSelector: PathSelector = pathSelector(globalGraph, typeRegistry) @@ -313,7 +339,7 @@ class UtBotSymbolicEngine( private val classUnderTest: ClassId = methodUnderTest.clazz.id - private val mocker: Mocker = Mocker(mockStrategy, classUnderTest, hierarchy, chosenClassesToMockAlways) + private val mocker: Mocker = Mocker(mockStrategy, classUnderTest, hierarchy, chosenClassesToMockAlways, MockListenerController(controller)) private val statesForConcreteExecution: MutableList = mutableListOf() @@ -348,6 +374,9 @@ class UtBotSymbolicEngine( private var queuedSymbolicStateUpdates = SymbolicStateUpdate() + private val featureProcessor: FeatureProcessor? = + if (enableFeatureProcess) EngineAnalyticsContext.featureProcessorFactory(globalGraph) else null + private val insideStaticInitializer get() = environment.state.executionStack.any { it.method.isStaticInitializer } @@ -370,6 +399,7 @@ class UtBotSymbolicEngine( logger.error(e) { "Closing resource failed" } } trackableResources.clear() + featureProcessor?.dumpFeatures() } private suspend fun preTraverse() { @@ -379,7 +409,9 @@ class UtBotSymbolicEngine( stateSelectedCount = 0 } - fun traverse(): Flow = traverseImpl().onStart { preTraverse() }.onCompletion { postTraverse() } + fun traverse(): Flow = traverseImpl() + .onStart { preTraverse() } + .onCompletion { postTraverse() } private fun traverseImpl(): Flow = flow { @@ -417,10 +449,13 @@ class UtBotSymbolicEngine( stateSelectedCount++ pathLogger.trace { "traverse<$methodUnderTest>: choosing next state($stateSelectedCount), " + - "queue size=${(pathSelector as? NonUniformRandomSearch)?.size ?: -1}" } + "queue size=${(pathSelector as? NonUniformRandomSearch)?.size ?: -1}" + } if (controller.executeConcretely || statesForConcreteExecution.isNotEmpty()) { - val state = pathSelector.pollUntilFastSAT() ?: statesForConcreteExecution.pollUntilSat() ?: break + val state = pathSelector.pollUntilFastSAT() + ?: statesForConcreteExecution.pollUntilSat(processUnknownStatesDuringConcreteExecution) + ?: break // This state can contain inconsistent wrappers - for example, Map with keys but missing values. // We cannot use withWrapperConsistencyChecks here because it needs solver to work. // So, we have to process such cases accurately in wrappers resolving. @@ -472,6 +507,7 @@ class UtBotSymbolicEngine( } else { val state = pathSelector.poll() + // state is null in case states queue is empty // or path selector exceed some limits (steps limit, for example) if (state == null) { @@ -491,32 +527,43 @@ class UtBotSymbolicEngine( } } - environment.state = state + state.executingTime += measureTimeMillis { + environment.state = state - val currentStmt = environment.state.stmt + val currentStmt = environment.state.stmt - environment.method = globalGraph.method(currentStmt) + if (currentStmt !in visitedStmts) { + environment.state.updateIsVisitedNew() + visitedStmts += currentStmt + } - environment.state.lastEdge?.let { - globalGraph.visitEdge(it) - } + environment.method = globalGraph.method(currentStmt) - try { - val exception = environment.state.exception - if (exception != null) { - traverseException(currentStmt, exception) - } else { - traverseStmt(currentStmt) + environment.state.lastEdge?.let { + globalGraph.visitEdge(it) } - } catch (ex: Throwable) { - environment.state.close() - if (ex !is CancellationException) { - logger.error(ex) { "Test generation failed on stmt $currentStmt, symbolic stack trace:\n$symbolicStackTrace" } - // TODO: enrich with nice description for known issues - emit(UtError(ex.description, ex)) - } else { - logger.debug(ex) { "Cancellation happened" } + try { + val exception = environment.state.exception + if (exception != null) { + traverseException(currentStmt, exception) + } else { + traverseStmt(currentStmt) + } + + // Here job can be cancelled from within traverse, e.g. by using force mocking without Mockito. + // So we need to make it throw CancelledException by method below: + currentCoroutineContext().job.ensureActive() + } catch (ex: Throwable) { + environment.state.close() + + if (ex !is CancellationException) { + logger.error(ex) { "Test generation failed on stmt $currentStmt, symbolic stack trace:\n$symbolicStackTrace" } + // TODO: enrich with nice description for known issues + emit(UtError(ex.description, ex)) + } else { + logger.debug(ex) { "Cancellation happened" } + } } } } @@ -527,11 +574,13 @@ class UtBotSymbolicEngine( } - //Simple fuzzing - fun fuzzing() = flow { - if (!UtSettings.useFuzzing) - return@flow - + /** + * Run fuzzing flow. + * + * @param until is used by fuzzer to cancel all tasks if the current time is over this value + * @param modelProvider provides model values for a method + */ + fun fuzzing(until: Long = Long.MAX_VALUE, modelProvider: (ModelProvider) -> ModelProvider = { it }) = flow { val executableId = if (methodUnderTest.isConstructor) { methodUnderTest.javaConstructor!!.executableId } else { @@ -546,6 +595,8 @@ class UtBotSymbolicEngine( return@flow } + val fallbackModelProvider = FallbackModelProvider { nextDefaultModelId++ } + val thisInstance = when { methodUnderTest.isStatic -> null methodUnderTest.isConstructor -> if ( @@ -557,7 +608,7 @@ class UtBotSymbolicEngine( null } else -> { - createSimpleModelByKClass(methodUnderTest.clazz).apply { + fallbackModelProvider.toModel(methodUnderTest.clazz).apply { if (this is UtNullModel) { // it will definitely fail because of NPE, return@flow } @@ -565,10 +616,21 @@ class UtBotSymbolicEngine( } } - val methodUnderTestDescription = FuzzedMethodDescription(executableId, collectConstantsForFuzzer(graph.body)) - val modelProvider = defaultModelProviders { nextDefaultModelId++ }.withFallback { createModelByClassId(it) } - fuzz(methodUnderTestDescription, modelProvider).forEachIndexed { index, parameters -> - val initialEnvironmentModels = EnvironmentModels(thisInstance, parameters, mapOf()) + val methodUnderTestDescription = FuzzedMethodDescription(executableId, collectConstantsForFuzzer(graph)).apply { + compilableName = if (methodUnderTest.isMethod) executableId.name else null + val names = graph.body.method.tags.filterIsInstance().firstOrNull()?.names + parameterNameMap = { index -> names?.getOrNull(index) } + } + val modelProviderWithFallback = modelProvider(defaultModelProviders { nextDefaultModelId++ }).withFallback(fallbackModelProvider::toModel) + val coveredInstructionTracker = mutableSetOf() + var attempts = UtSettings.fuzzingMaxAttempts + fuzz(methodUnderTestDescription, modelProviderWithFallback).forEach { values -> + if (System.currentTimeMillis() >= until) { + logger.info { "Fuzzing overtime: $methodUnderTest" } + return@flow + } + + val initialEnvironmentModels = EnvironmentModels(thisInstance, values.map { it.model }, mapOf()) try { val concreteExecutionResult = @@ -583,6 +645,20 @@ class UtBotSymbolicEngine( } } + if (!coveredInstructionTracker.addAll(concreteExecutionResult.coverage.coveredInstructions)) { + if (--attempts < 0) { + return@flow + } + } + + val nameSuggester = sequenceOf(ModelBasedNameSuggester(), MethodBasedNameSuggester()) + val testMethodName = try { + nameSuggester.flatMap { it.suggest(methodUnderTestDescription, values, concreteExecutionResult.result) }.firstOrNull() + } catch (t: Throwable) { + logger.error(t) { "Cannot create suggested test name for ${methodUnderTest.displayName}" } + null + } + emit( UtExecution( stateBefore = initialEnvironmentModels, @@ -592,7 +668,8 @@ class UtBotSymbolicEngine( path = mutableListOf(), fullPath = emptyList(), coverage = concreteExecutionResult.coverage, - testMethodName = "test${methodUnderTest.callable.name.capitalize()}ByFuzzer${index}" + testMethodName = testMethodName?.testName, + displayName = testMethodName?.displayName ) ) } catch (e: CancellationException) { @@ -605,86 +682,6 @@ class UtBotSymbolicEngine( } } - /** - * Finds constant values in method body. - * - * todo move to utbot-fuzzer module [module should have access to some API to traverse through control-flow graph] - */ - private fun collectConstantsForFuzzer(body: Body): MutableList { - val constants = mutableListOf() - - body.units.forEach { unit -> - unit.useBoxes.asSequence().map { it.value }.filter { it is Constant || it is JCastExpr }.forEach { value -> - try { - var constant: FuzzedConcreteValue? = null - if (unit is JIfStmt && value is Constant) { - /* - * It is acceptable to check different types in if statement like ```2 == 2L```. - * - * Because of that fuzzer tries to find out the correct type between local and constant. - * Constant should be converted into type of local var in such way that if-statement can be true. - */ - val exactValue = value.javaClass.getField("value")[value] - val local = unit.conditionBox.value.useBoxes.map { it.value } - .filterIsInstance() - .takeIf { it.size == 1 } - ?.first() - if (local?.type != value.type && value.type is IntType) { - // Soot loads any integer type as an Int, - // therefore we try to guess target type using second value - // in the if statement - constant = when (local?.type) { - is CharType -> FuzzedConcreteValue(charClassId, (exactValue as Int).toChar()) - is BooleanType -> FuzzedConcreteValue(booleanClassId, (exactValue == 1)) - is ByteType -> FuzzedConcreteValue(byteClassId, (exactValue as Int).toByte()) - is ShortType -> FuzzedConcreteValue(shortClassId, (exactValue as Int).toShort()) - else -> null - } - } - } - /* - * The if-statement with 8-byte types like long and double doesn't use simple comparison, - * but cast and `cmp` method instead. This block tries to recognize this situation - * and cast wider type to more narrow one. - */ - if (value is JCastExpr) { - val next = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() - if (next is JAssignStmt) { - val const = next.useBoxes.map { it.value } - .filterIsInstance() - .takeIf { it.size == 1 } - ?.first() - if (const != null) { - val exactValue = const.javaClass.getField("value")[const] as Number - constant = when (value.op.type) { - is ByteType -> FuzzedConcreteValue(byteClassId, exactValue.toByte()) - is ShortType -> FuzzedConcreteValue(shortClassId, exactValue.toShort()) - is IntType -> FuzzedConcreteValue(intClassId, exactValue.toInt()) - is FloatType -> FuzzedConcreteValue(floatClassId, exactValue.toFloat()) - else -> null - } - } - } - } - /* - * Load constant as is. - */ - if (constant == null && value is Constant) { - val exactValue = value.javaClass.getField("value")[value] - constant = FuzzedConcreteValue(value.type.classId, exactValue) - } - - if (constant != null) { - constants += constant - } - } catch (e: Exception) { - logger.warn(e) { "Cannot process constant value of type '${value.type}}'" } - } - } - } - return constants - } - private suspend fun FlowCollector.emitFailedConcreteExecutionResult( stateBefore: EnvironmentModels, e: ConcreteExecutionFailureException @@ -702,70 +699,6 @@ class UtBotSymbolicEngine( } - private fun createModelByClassId(classId: ClassId): UtModel { - val modelConstructor = UtModelConstructor(IdentityHashMap()) - val defaultConstructor = classId.jClass.constructors.firstOrNull { - it.parameters.isEmpty() && it.isPublic - } - return when { - classId.isPrimitive -> - classId.defaultValueModel() - classId.isArray -> - UtArrayModel( - id = nextDefaultModelId++, - classId, - length = 0, - classId.elementClassId!!.defaultValueModel(), - mutableMapOf() - ) - classId.isIterable -> { - val defaultInstance = when { - defaultConstructor != null -> defaultConstructor.newInstance() - classId.jClass.isAssignableFrom(java.util.ArrayList::class.java) -> ArrayList() - classId.jClass.isAssignableFrom(java.util.TreeSet::class.java) -> TreeSet() - classId.jClass.isAssignableFrom(java.util.HashMap::class.java) -> HashMap() - classId.jClass.isAssignableFrom(java.util.ArrayDeque::class.java) -> ArrayDeque() - classId.jClass.isAssignableFrom(java.util.BitSet::class.java) -> BitSet() - else -> null - } - if (defaultInstance != null) - modelConstructor.construct(defaultInstance, classId) - else - createSimpleModelByKClass(classId.kClass) - } - else -> - createSimpleModelByKClass(classId.kClass) - } - } - - private fun createSimpleModelByKClass(kclass: KClass<*>): UtModel { - val defaultConstructor = kclass.java.constructors.firstOrNull { - it.parameters.isEmpty() && it.isPublic // check constructor is public - } - return if (kclass.isAbstract) { // sealed class is abstract by itself - UtNullModel(kclass.java.id) - } else if (defaultConstructor != null) { - val chain = mutableListOf() - val model = UtAssembleModel( - id = nextDefaultModelId++, - kclass.id, - kclass.id.toString(), - chain - ) - chain.add( - UtExecutableCallModel(model, defaultConstructor.executableId, listOf(), model) - ) - model - } else { - UtCompositeModel( - id = nextDefaultModelId++, - kclass.id, - isMock = false - ) - } - } - - private suspend fun FlowCollector.traverseStmt(current: Stmt) { if (doPreparatoryWorkIfRequired(current)) return @@ -876,7 +809,7 @@ class UtBotSymbolicEngine( stmt: Stmt ): Boolean { if (shouldProcessStaticFieldConcretely(fieldRef)) { - return processStaticFieldConcretely(fieldRef) + return processStaticFieldConcretely(fieldRef, stmt) } val field = fieldRef.field @@ -922,7 +855,9 @@ class UtBotSymbolicEngine( // Note that this list is not exhaustive, so it may be supplemented in the future. val packagesToProcessConcretely = javaPackagesToProcessConcretely + sunPackagesToProcessConcretely - return packagesToProcessConcretely.any { className.startsWith(it) } + val declaringClass = fieldRef.field.declaringClass + + val isFromPackageToProcessConcretely = packagesToProcessConcretely.any { className.startsWith(it) } // it is required to remove classes we override, since // we could accidentally initialize their final fields // with values that will later affect our overridden classes @@ -933,6 +868,13 @@ class UtBotSymbolicEngine( //hardcoded string for class name is used cause class is not public //this is a hack to avoid crashing on code with Math.random() && !className.endsWith("RandomNumberGeneratorHolder") + + // we can process concretely only enums that does not affect the external system + val isEnumNotAffectingExternalStatics = declaringClass.let { + it.isEnum && !it.isEnumAffectingExternalStatics(typeResolver) + } + + return isEnumNotAffectingExternalStatics || isFromPackageToProcessConcretely } } @@ -954,7 +896,7 @@ class UtBotSymbolicEngine( * * Returns true if processing takes place and Engine should end traversal of current statement. */ - private fun processStaticFieldConcretely(fieldRef: StaticFieldRef): Boolean { + private fun processStaticFieldConcretely(fieldRef: StaticFieldRef, stmt: Stmt): Boolean { val field = fieldRef.field val fieldId = field.fieldId if (memory.isInitialized(fieldId)) { @@ -963,6 +905,107 @@ class UtBotSymbolicEngine( // Gets concrete value, converts to symbolic value val declaringClass = field.declaringClass + + val (edge, updates) = if (declaringClass.isEnum) { + makeConcreteUpdatesForEnums(fieldId, declaringClass, stmt) + } else { + makeConcreteUpdatesForNonEnumStaticField(field, fieldId, declaringClass) + } + + val newState = environment.state.updateQueued(edge, updates) + pathSelector.offer(newState) + + return true + } + + private fun makeConcreteUpdatesForEnums( + fieldId: FieldId, + declaringClass: SootClass, + stmt: Stmt + ): Pair { + val type = declaringClass.type + val jClass = type.id.jClass + + // symbolic value for enum class itself + val enumClassValue = findOrCreateStaticObject(type) + + // values for enum constants + val enumConstantConcreteValues = jClass.enumConstants.filterIsInstance>() + + val (enumConstantSymbolicValues, enumConstantSymbolicResultsByName) = + makeSymbolicValuesFromEnumConcreteValues(type, enumConstantConcreteValues) + + val enumFields = typeResolver.findFields(type) + + val sootFieldsWithRuntimeValues = + associateEnumSootFieldsWithConcreteValues(enumFields, enumConstantConcreteValues) + + val (staticFields, nonStaticFields) = sootFieldsWithRuntimeValues.partition { it.first.isStatic } + + val (staticFieldUpdates, curFieldSymbolicValueForLocalVariable) = makeEnumStaticFieldsUpdates( + staticFields, + declaringClass, + enumConstantSymbolicResultsByName, + enumConstantSymbolicValues, + enumClassValue, + fieldId + ) + + val nonStaticFieldsUpdates = makeEnumNonStaticFieldsUpdates(enumConstantSymbolicValues, nonStaticFields) + + // we do not mark static fields for enum constants and $VALUES as meaningful + // because we should not set them in generated code + val meaningfulStaticFields = staticFields.filterNot { + val name = it.first.name + + name in enumConstantSymbolicResultsByName.keys || isEnumValuesFieldName(name) + } + + val initializedStaticFieldsMemoryUpdate = MemoryUpdate( + initializedStaticFields = staticFields.associate { it.first.fieldId to it.second.single() }.toPersistentMap(), + meaningfulStaticFields = meaningfulStaticFields.map { it.first.fieldId }.toPersistentSet() + ) + + var allUpdates = staticFieldUpdates + nonStaticFieldsUpdates + initializedStaticFieldsMemoryUpdate + + // we need to make locals update if it is an assignment statement + // for enums we have only two types for assignment with enums β€” enum constant or $VALUES field + // for example, a jimple body for Enum::values method starts with the following lines: + // public static ClassWithEnum$StatusEnum[] values() + // { + // ClassWithEnum$StatusEnum[] $r0, $r2; + // java.lang.Object $r1; + // $r0 = ; + // $r1 = virtualinvoke $r0.(); + + // so, we have to make an update for the local $r0 + if (stmt is JAssignStmt) { + val local = stmt.leftOp as JimpleLocal + val localUpdate = localMemoryUpdate( + local.variable to curFieldSymbolicValueForLocalVariable + ) + + allUpdates += localUpdate + } + + // enum static initializer can be the first statement in method so there will be no last edge + // for example, as it is during Enum::values method analysis: + // public static ClassWithEnum$StatusEnum[] values() + // { + // ClassWithEnum$StatusEnum[] $r0, $r2; + // java.lang.Object $r1; + + // $r0 = ; + val edge = environment.state.lastEdge ?: globalGraph.succ(stmt) + + return edge to allUpdates + } + + private fun makeConcreteUpdatesForNonEnumStaticField( + field: SootField, + fieldId: FieldId, + declaringClass: SootClass + ): Pair { val concreteValue = extractConcreteValue(field, declaringClass) val (symbolicResult, symbolicStateUpdate) = toMethodResult(concreteValue, field.type) val symbolicValue = (symbolicResult as SymbolicSuccess).value @@ -970,19 +1013,15 @@ class UtBotSymbolicEngine( // Collects memory updates val initializedFieldUpdate = MemoryUpdate(initializedStaticFields = persistentHashMapOf(fieldId to concreteValue)) + val objectUpdate = objectUpdate( instance = findOrCreateStaticObject(declaringClass.type), field = field, value = valueToExpression(symbolicValue, field.type) ) val allUpdates = symbolicStateUpdate + initializedFieldUpdate + objectUpdate - pathSelector.offer( - environment.state.updateQueued( - edge = environment.state.lastEdge!!, - allUpdates - ) - ) - return true + + return environment.state.lastEdge!! to allUpdates } // Some fields are inaccessible with reflection, so we have to instantiate it by ourselves. @@ -1200,7 +1239,7 @@ class UtBotSymbolicEngine( /** * Converts value to expression with cast to target type for primitives. */ - private fun valueToExpression(value: SymbolicValue, type: Type): UtExpression = when (value) { + fun valueToExpression(value: SymbolicValue, type: Type): UtExpression = when (value) { is ReferenceValue -> value.addr // TODO: shall we add additional constraint that aligned expression still equals original? // BitVector can lose valuable bites during extraction @@ -1357,6 +1396,8 @@ class UtBotSymbolicEngine( queuedSymbolicStateUpdates += typeRegistry.genericTypeParameterConstraint(value.addr, typeStorages).asHardConstraint() parameterAddrToGenericType += value.addr to type + + typeRegistry.saveObjectParameterTypeStorages(value.addr, typeStorages) } } @@ -1369,28 +1410,67 @@ class UtBotSymbolicEngine( val (positiveCaseSoftConstraint, negativeCaseSoftConstraint) = resolvedCondition.softConstraints val negativeCasePathConstraint = mkNot(positiveCasePathConstraint) - positiveCaseEdge?.let { edge -> - environment.state.expectUndefined() - val positiveCaseState = environment.state.updateQueued( - edge, - SymbolicStateUpdate( - hardConstraints = positiveCasePathConstraint.asHardConstraint(), - softConstraints = setOfNotNull(positiveCaseSoftConstraint).asSoftConstraint() - ) + resolvedCondition.symbolicStateUpdates.positiveCase - ) - pathSelector.offer(positiveCaseState) + if (positiveCaseEdge != null) { + environment.state.definitelyFork() + } + + /* assumeOrExecuteConcrete in jimple looks like: + ``` z0 = a > 5 + if (z0 == 1) goto label1 + assumeOrExecuteConcretely(z0) + + label1: + assumeOrExecuteConcretely(z0) + ``` + + We have to detect such situations to avoid addition `a > 5` into hardConstraints, + because we want to add them into Assumptions. + + Note: we support only simple predicates right now (one logical operation), + otherwise they will be added as hard constraints, and we will not execute + the state concretely if there will be UNSAT because of assumptions. + */ + val isAssumeExpr = positiveCaseEdge?.let { isConditionForAssumeOrExecuteConcretely(it.dst) } ?: false + + // in case of assume we want to have the only branch where $z = 1 (it is a negative case) + if (!isAssumeExpr) { + positiveCaseEdge?.let { edge -> + environment.state.expectUndefined() + val positiveCaseState = environment.state.updateQueued( + edge, + SymbolicStateUpdate( + hardConstraints = positiveCasePathConstraint.asHardConstraint(), + softConstraints = setOfNotNull(positiveCaseSoftConstraint).asSoftConstraint() + ) + resolvedCondition.symbolicStateUpdates.positiveCase + ) + pathSelector.offer(positiveCaseState) + } } + // Depending on existance of assumeExpr we have to add corresponding hardConstraints and assumptions + val hardConstraints = if (!isAssumeExpr) negativeCasePathConstraint.asHardConstraint() else HardConstraint() + val assumption = if (isAssumeExpr) negativeCasePathConstraint.asAssumption() else Assumption() + val negativeCaseState = environment.state.updateQueued( negativeCaseEdge, SymbolicStateUpdate( - hardConstraints = negativeCasePathConstraint.asHardConstraint(), + hardConstraints = hardConstraints, softConstraints = setOfNotNull(negativeCaseSoftConstraint).asSoftConstraint(), + assumptions = assumption ) + resolvedCondition.symbolicStateUpdates.negativeCase ) pathSelector.offer(negativeCaseState) } + /** + * Returns true if the next stmt is an [assumeOrExecuteConcretelyMethod] invocation, false otherwise. + */ + private fun isConditionForAssumeOrExecuteConcretely(stmt: Stmt): Boolean { + val successor = globalGraph.succStmts(stmt).singleOrNull() as? JInvokeStmt ?: return false + val invokeExpression = successor.invokeExpr as? JStaticInvokeExpr ?: return false + return invokeExpression.method.isUtMockAssumeOrExecuteConcretely + } + private fun traverseInvokeStmt(current: JInvokeStmt) { val results = invokeResult(current.invokeExpr) @@ -1438,7 +1518,9 @@ class UtBotSymbolicEngine( } if (successors.size > 1) { environment.state.expectUndefined() + environment.state.definitelyFork() } + successors.forEach { (target, expr) -> pathSelector.offer( environment.state.updateQueued( @@ -2028,6 +2110,7 @@ class UtBotSymbolicEngine( val touchedStaticFields = persistentListOf(staticFieldMemoryUpdate) queuedSymbolicStateUpdates += MemoryUpdate(staticFieldsUpdates = touchedStaticFields) + // TODO filter enum constant static fields JIRA:1681 if (!environment.method.isStaticInitializer && !fieldId.isSynthetic) { queuedSymbolicStateUpdates += MemoryUpdate(meaningfulStaticFields = persistentSetOf(fieldId)) } @@ -2144,8 +2227,15 @@ class UtBotSymbolicEngine( val chunkId = hierarchy.chunkIdForField(objectType, field) val createdField = createField(objectType, addr, field.type, chunkId, mockInfoGenerator) - if (field.type is RefLikeType && field.shouldBeNotNull()) { - queuedSymbolicStateUpdates += mkNot(mkEq(createdField.addr, nullObjectAddr)).asHardConstraint() + if (field.type is RefLikeType) { + if (field.shouldBeNotNull()) { + queuedSymbolicStateUpdates += mkNot(mkEq(createdField.addr, nullObjectAddr)).asHardConstraint() + } + + // See docs/SpeculativeFieldNonNullability.md for details + if (field.isFinal && field.declaringClass.isLibraryClass && !checkNpeForFinalFields) { + markAsSpeculativelyNotNull(createdField.addr) + } } return createdField @@ -2315,6 +2405,10 @@ class UtBotSymbolicEngine( queuedSymbolicStateUpdates += MemoryUpdate(touchedAddresses = persistentListOf(addr)) } + private fun markAsSpeculativelyNotNull(addr: UtAddrExpression) { + queuedSymbolicStateUpdates += MemoryUpdate(speculativelyNotNullAddresses = persistentListOf(addr)) + } + /** * Add a memory update to reflect that a field was read. * @@ -2377,7 +2471,7 @@ class UtBotSymbolicEngine( is JInterfaceInvokeExpr -> virtualAndInterfaceInvoke(invokeExpr.base, invokeExpr.methodRef, invokeExpr.args) is JVirtualInvokeExpr -> virtualAndInterfaceInvoke(invokeExpr.base, invokeExpr.methodRef, invokeExpr.args) is JSpecialInvokeExpr -> specialInvoke(invokeExpr) - is JDynamicInvokeExpr -> TODO("$invokeExpr") + is JDynamicInvokeExpr -> dynamicInvoke(invokeExpr) else -> error("Unknown class ${invokeExpr::class}") } @@ -2442,7 +2536,8 @@ class UtBotSymbolicEngine( if (methodSignature != makeSymbolicMethod.signature && methodSignature != nonNullableMakeSymbolic.signature) return null val method = environment.method - val isInternalMock = method.hasInternalMockAnnotation || method.declaringClass.allMethodsAreInternalMocks + val declaringClass = method.declaringClass + val isInternalMock = method.hasInternalMockAnnotation || declaringClass.allMethodsAreInternalMocks || declaringClass.isOverridden val parameters = resolveParameters(invokeExpr.args, invokeExpr.method.parameterTypes) val mockMethodResult = mockStaticMethod(invokeExpr.method, parameters)?.single() ?: error("Unsuccessful mock attempt of the `makeSymbolic` method call: $invokeExpr") @@ -2474,6 +2569,8 @@ class UtBotSymbolicEngine( ) } + fun attachMockListener(mockListener: MockListener) = mocker.mockListenerController?.attach(mockListener) + private fun staticInvoke(invokeExpr: JStaticInvokeExpr): List { val parameters = resolveParameters(invokeExpr.args, invokeExpr.method.parameterTypes) val result = mockMakeSymbolic(invokeExpr) ?: mockStaticMethod(invokeExpr.method, parameters) @@ -2634,6 +2731,15 @@ class UtBotSymbolicEngine( return commonInvokePart(invocation) } + private fun dynamicInvoke(invokeExpr: JDynamicInvokeExpr): List { + workaround(HACK) { + // The engine does not yet support JDynamicInvokeExpr, so switch to concrete execution if we encounter it + statesForConcreteExecution += environment.state + queuedSymbolicStateUpdates += UtFalse.asHardConstraint() + return emptyList() + } + } + /** * Runs common invocation part for object wrapper or object instance. * @@ -2645,6 +2751,10 @@ class UtBotSymbolicEngine( // If so, return the result of the override if (artificialMethodOverride.success) { + if (artificialMethodOverride.results.size > 1) { + environment.state.definitelyFork() + } + return mutableListOf().apply { for (result in artificialMethodOverride.results) { when (result) { @@ -2672,6 +2782,10 @@ class UtBotSymbolicEngine( // that will override the invocation. val overrideResults = targets.map { it to overrideInvocation(invocation, it) } + if (overrideResults.sumOf { (_, overriddenResult) -> overriddenResult.results.size } > 1) { + environment.state.definitelyFork() + } + // Separate targets for which invocation should be overridden // from the targets that should be processed regularly. val (overridden, original) = overrideResults.partition { it.second.success } @@ -2708,19 +2822,39 @@ class UtBotSymbolicEngine( return overriddenResults + originResults } - private fun invoke(target: InvocationTarget, parameters: List): List { - val substitutedMethod = typeRegistry.findSubstitutionOrNull(target.method) + private fun invoke( + target: InvocationTarget, + parameters: List + ): List = with(target.method) { + val substitutedMethod = typeRegistry.findSubstitutionOrNull(this) - if (target.method.isNative && substitutedMethod == null) return processNativeMethod(target) + if (isNative && substitutedMethod == null) return processNativeMethod(target) - // If we face UtMock.assume call, we should continue only with the branch where the predicate - // from the parameters is equal true - return when { - target.method.isUtMockAssume -> { + // If we face UtMock.assume call, we should continue only with the branch + // where the predicate from the parameters is equal true + when { + isUtMockAssume || isUtMockAssumeOrExecuteConcretely -> { val param = UtCastExpression(parameters.single() as PrimitiveValue, BooleanType.v()) + + val assumptionStmt = mkEq(param, UtTrue) + val (hardConstraints, assumptions) = if (isUtMockAssume) { + // For UtMock.assume we must add the assumeStmt to the hard constraints + setOf(assumptionStmt) to emptySet() + } else { + // For assumeOrExecuteConcretely we must add the statement to the assumptions. + // It is required to have opportunity to remove it later in case of unsat state + // because of it and execute the state concretely. + emptySet() to setOf(assumptionStmt) + } + + val symbolicStateUpdate = SymbolicStateUpdate( + hardConstraints = hardConstraints.asHardConstraint(), + assumptions = assumptions.asAssumption() + ) + val stateToContinue = environment.state.updateQueued( globalGraph.succ(environment.state.stmt), - SymbolicStateUpdate(hardConstraints = mkEq(param, UtTrue).asHardConstraint()) + symbolicStateUpdate ) pathSelector.offer(stateToContinue) @@ -2729,17 +2863,15 @@ class UtBotSymbolicEngine( queuedSymbolicStateUpdates += UtFalse.asHardConstraint() emptyList() } - target.method.declaringClass == utOverrideMockClass -> utOverrideMockInvoke(target, parameters) - target.method.declaringClass == utLogicMockClass -> utLogicMockInvoke(target, parameters) - target.method.declaringClass == utArrayMockClass -> utArrayMockInvoke(target, parameters) + declaringClass == utOverrideMockClass -> utOverrideMockInvoke(target, parameters) + declaringClass == utLogicMockClass -> utLogicMockInvoke(target, parameters) + declaringClass == utArrayMockClass -> utArrayMockInvoke(target, parameters) else -> { - val graph = substitutedMethod?.jimpleBody()?.graph() ?: target.method.jimpleBody().graph() - val isLibraryMethod = target.method.isLibraryMethod + val graph = substitutedMethod?.jimpleBody()?.graph() ?: jimpleBody().graph() pushToPathSelector(graph, target.instance, parameters, target.constraints, isLibraryMethod) emptyList() } } - } private fun utOverrideMockInvoke(target: InvocationTarget, parameters: List): List { @@ -2957,6 +3089,11 @@ class UtBotSymbolicEngine( instanceAsWrapperOrNull?.run { val results = invoke(instance as ObjectValue, invocation.method, invocation.parameters) + if (results.isEmpty()) { + // Drop the branch and switch to concrete execution + statesForConcreteExecution += environment.state + queuedSymbolicStateUpdates += UtFalse.asHardConstraint() + } return OverrideResult(success = true, results) } @@ -3207,9 +3344,11 @@ class UtBotSymbolicEngine( private fun nullPointerExceptionCheck(addr: UtAddrExpression) { val canBeNull = addrEq(addr, nullObjectAddr) val canNotBeNull = mkNot(canBeNull) + val notMarked = mkEq(memory.isSpeculativelyNotNull(addr), mkFalse()) + val notMarkedAndNull = mkAnd(notMarked, canBeNull) if (environment.method.checkForNPE(environment.state.executionStack.size)) { - implicitlyThrowException(NullPointerException(), setOf(canBeNull)) + implicitlyThrowException(NullPointerException(), setOf(notMarkedAndNull)) } queuedSymbolicStateUpdates += canNotBeNull.asHardConstraint() @@ -3334,12 +3473,12 @@ class UtBotSymbolicEngine( mkNot(UtSubNoOverflowExpression(left.expr, right.expr)) } is JMulExpr -> when (sort.type) { - is ByteType -> lowerIntMulOverflowCheck(left, right, Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()) - is ShortType -> lowerIntMulOverflowCheck(left, right, Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()) - is IntType -> higherIntMulOverflowCheck(left, right, Int.SIZE_BITS, Int.MIN_VALUE.toLong()) { it: UtExpression -> it.toIntValue() } - is LongType -> higherIntMulOverflowCheck(left, right, Long.SIZE_BITS, Long.MIN_VALUE) { it: UtExpression -> it.toLongValue() } - else -> null - } + is ByteType -> lowerIntMulOverflowCheck(left, right, Byte.MIN_VALUE.toInt(), Byte.MAX_VALUE.toInt()) + is ShortType -> lowerIntMulOverflowCheck(left, right, Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt()) + is IntType -> higherIntMulOverflowCheck(left, right, Int.SIZE_BITS, Int.MIN_VALUE.toLong()) { it: UtExpression -> it.toIntValue() } + is LongType -> higherIntMulOverflowCheck(left, right, Long.SIZE_BITS, Long.MIN_VALUE) { it: UtExpression -> it.toLongValue() } + else -> null + } else -> null } @@ -3399,18 +3538,10 @@ class UtBotSymbolicEngine( } } - val numDimensions = typeAfterCast.numDimensions - val inheritors = if (baseTypeAfterCast is PrimType) { - setOf(typeAfterCast) - } else { - typeResolver - .findOrConstructInheritorsIncludingTypes(baseTypeAfterCast as RefType) - .mapTo(mutableSetOf()) { if (numDimensions > 0) it.makeArrayType(numDimensions) else it } - } - val preferredTypesForCastException = valueToCast.possibleConcreteTypes.filterNot { it in inheritors } + val inheritorsTypeStorage = typeResolver.constructTypeStorage(typeAfterCast, useConcreteType = false) + val preferredTypesForCastException = valueToCast.possibleConcreteTypes.filterNot { it in inheritorsTypeStorage.possibleConcreteTypes } - val typeStorage = typeResolver.constructTypeStorage(typeAfterCast, inheritors) - val isExpression = typeRegistry.typeConstraint(addr, typeStorage).isConstraint() + val isExpression = typeRegistry.typeConstraint(addr, inheritorsTypeStorage).isConstraint() val notIsExpression = mkNot(isExpression) val nullEqualityConstraint = addrEq(addr, nullObjectAddr) @@ -3644,7 +3775,10 @@ class UtBotSymbolicEngine( // Still, we need overflows to act as implicit exceptions. (UtSettings.treatOverflowAsError && symbolicExecutionResult is UtOverflowFailure) ) { - logger.debug { "processResult<${methodUnderTest}>: no concrete execution allowed, emit purely symbolic result" } + logger.debug { + "processResult<${methodUnderTest}>: no concrete execution allowed, " + + "emit purely symbolic result $symbolicUtExecution" + } emit(symbolicUtExecution) return } 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 93c1122802..06928beda1 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/ValueConstructor.kt @@ -3,6 +3,7 @@ package org.utbot.engine import org.utbot.common.findField import org.utbot.common.findFieldOrNull import org.utbot.common.invokeCatching +import org.utbot.common.withAccessibility import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.ConstructorId import org.utbot.framework.plugin.api.EnvironmentModels @@ -390,12 +391,19 @@ class ValueConstructor { */ private fun value(model: UtModel) = construct(model, null).value - private fun MethodId.call(args: List, instance: Any?): Any? = - method.invokeCatching(obj = instance, args = args).getOrThrow() + method.run { + withAccessibility { + invokeCatching(obj = instance, args = args).getOrThrow() + } + } private fun ConstructorId.call(args: List): Any? = - constructor.newInstance(*args.toTypedArray()) + constructor.run { + withAccessibility { + newInstance(*args.toTypedArray()) + } + } /** * Fetches primitive value from NutsModel to create array of primitives. diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Query.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Query.kt index 6d64c0263e..51e41560ba 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Query.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Query.kt @@ -119,7 +119,8 @@ data class Query( lts: Map = this@Query.lts, gts: Map = this@Query.gts ) = AxiomInstantiationRewritingVisitor(eqs, lts, gts).let { visitor -> - this.map { it.simplify(visitor) }.map { simplifyGeneric(it) } + this.map { it.simplify(visitor) } + .map { simplifyGeneric(it) } .flatMap { splitAnd(it) } + visitor.instantiatedArrayAxioms } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolver.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolver.kt index 1ea07cfcba..d8dd4d0682 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolver.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/UtSolver.kt @@ -12,6 +12,8 @@ import org.utbot.engine.TypeRegistry import org.utbot.engine.pc.UtSolverStatusKind.SAT import org.utbot.engine.pc.UtSolverStatusKind.UNKNOWN import org.utbot.engine.pc.UtSolverStatusKind.UNSAT +import org.utbot.engine.prettify +import org.utbot.engine.symbolic.Assumption import org.utbot.engine.symbolic.HardConstraint import org.utbot.engine.symbolic.SoftConstraint import org.utbot.engine.prettify @@ -126,6 +128,11 @@ data class UtSolver constructor( //these constraints.hard are already added to z3solver private var constraints: BaseQuery = Query(), + // Constraints that should not be added in the solver as hypothesis. + // Instead, we use `check` to find out if they are satisfiable. + // It is required to have unsat cores with them. + var assumption: Assumption = Assumption(), + //new constraints for solver (kind of incremental behavior) private var hardConstraintsNotYetAddedToZ3Solver: PersistentSet = persistentHashSetOf(), @@ -136,6 +143,11 @@ data class UtSolver constructor( private val translator: Z3TranslatorVisitor = Z3TranslatorVisitor(context, typeRegistry) + /** + * Constraints from the [assumption] that are not satisfiable. + * All the elements were found in the calculated unsat core. + */ + internal val failedAssumptions = mutableListOf() //protection against solver reusage private var canBeCloned: Boolean = true @@ -163,13 +175,13 @@ data class UtSolver constructor( var expectUndefined: Boolean = false - fun add(hard: HardConstraint, soft: SoftConstraint): UtSolver { + fun add(hard: HardConstraint, soft: SoftConstraint, assumption: Assumption = Assumption()): UtSolver { // status can implicitly change here to UNDEFINED or UNSAT val newConstraints = constraints.with(hard.constraints, soft.constraints) val wantClone = (expectUndefined && newConstraints.status is UtSolverStatusUNDEFINED) || (!expectUndefined && newConstraints.status !is UtSolverStatusUNSAT) - return if (wantClone && canBeCloned) { + return if (wantClone && canBeCloned && assumption.constraints.isEmpty()) { // try to reuse z3 Solver with value SAT when possible canBeCloned = false copy( @@ -177,13 +189,22 @@ data class UtSolver constructor( hardConstraintsNotYetAddedToZ3Solver = hardConstraintsNotYetAddedToZ3Solver.addAll(newConstraints.lastAdded), ) } else { + // We pass here undefined status to force calculation + // at the next `check` call. Otherwise, we'd ignore + // the given assumptions and return already calculated status. + val constraintsWithStatus = if (assumption.constraints.isNotEmpty()) { + newConstraints.withStatus(UtSolverStatusUNDEFINED) + } else { + newConstraints + } /* Create new solver to add another constraints (new branches) New solver hasn't already added constraints thus we must add them again */ copy( - constraints = newConstraints, + constraints = constraintsWithStatus, hardConstraintsNotYetAddedToZ3Solver = newConstraints.hard, + assumption = this.assumption + assumption, z3Solver = context.mkSolver().also { it.setParameters(params) }, ) } @@ -195,11 +216,12 @@ data class UtSolver constructor( } val translatedSoft = if (respectSoft && preferredCexOption) { - constraints.soft.associateByTo(mutableMapOf()) { translator.translate(it) as BoolExpr } + constraints.soft.translate() } else { mutableMapOf() } + val translatedAssumes = assumption.constraints.translate() val statusHolder = logger.trace().bracket("High level check(): ", { it }) { Predictors.smtIncremental.learnOn(IncrementalData(constraints.hard, hardConstraintsNotYetAddedToZ3Solver)) { @@ -210,7 +232,7 @@ data class UtSolver constructor( "${str.md5()}\n$str" } - when (val status = check(translatedSoft)) { + when (val status = check(translatedSoft, translatedAssumes)) { SAT -> UtSolverStatusSAT(translator, z3Solver) else -> UtSolverStatusUNSAT(status) } @@ -227,19 +249,31 @@ data class UtSolver constructor( z3Solver.reset() } - private fun check(translatedSoft: MutableMap): UtSolverStatusKind { + private fun check( + translatedSoft: MutableMap, + translatedAssumptions: MutableMap + ): UtSolverStatusKind { + val assumptionsInUnsatCore = mutableListOf() + while (true) { val res = logger.trace().bracket("Low level check(): ", { it }) { - z3Solver.check(*translatedSoft.keys.toTypedArray()) + val constraintsToCheck = translatedSoft.keys + translatedAssumptions.keys + z3Solver.check(*constraintsToCheck.toTypedArray()) } when (res) { - SATISFIABLE -> return SAT + SATISFIABLE -> { + if (assumptionsInUnsatCore.isNotEmpty()) { + failedAssumptions += assumptionsInUnsatCore + } + + return SAT + } UNSATISFIABLE -> { val unsatCore = z3Solver.unsatCore // if we don't have any soft constraints and enabled unsat cores // for hard constraints, then calculate it and print the result using the logger - if (translatedSoft.isEmpty() && UtSettings.enableUnsatCoreCalculationForHardConstraints) { + if (translatedSoft.isEmpty() && translatedAssumptions.isEmpty() && UtSettings.enableUnsatCoreCalculationForHardConstraints) { with(context.mkSolver()) { check(*z3Solver.assertions) val constraintsInUnsatCore = this.unsatCore.toList() @@ -253,15 +287,26 @@ data class UtSolver constructor( // an unsat core for hard constraints if (unsatCore.isEmpty()) return UNSAT - for (unsatVariable in unsatCore) { - translatedSoft.remove(unsatVariable) - ?: error("$unsatVariable from unsatCore isn't in soft constraints") + val failedSoftConstraints = unsatCore.filter { it in translatedSoft.keys } + + if (failedSoftConstraints.isNotEmpty()) { + failedSoftConstraints.forEach { translatedSoft.remove(it) } + // remove soft constraints first, only then try to remove assumptions + continue } + + unsatCore + .filter { it in translatedAssumptions.keys } + .forEach { + assumptionsInUnsatCore += translatedAssumptions.getValue(it) + translatedAssumptions.remove(it) + } } else -> { logger.debug { "Reason of UNKNOWN: ${z3Solver.reasonUnknown}" } if (translatedSoft.isEmpty()) { - logger.debug {"No soft constraints left, return UNKNOWN"} + logger.debug { "No soft constraints left, return UNKNOWN" } + logger.trace { "Constraints lead to unknown: ${z3Solver.assertions.joinToString("\n")} " } return UNKNOWN } @@ -270,6 +315,9 @@ data class UtSolver constructor( } } } + + private fun Collection.translate(): MutableMap = + associateByTo(mutableMapOf()) { translator.translate(it) as BoolExpr } } enum class UtSolverStatusKind { diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3TranslatorVisitor.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3TranslatorVisitor.kt index 0a8aea1f0b..4f0d7516d4 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3TranslatorVisitor.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/pc/Z3TranslatorVisitor.kt @@ -227,9 +227,11 @@ open class Z3TranslatorVisitor( val constraints = mutableListOf() for (i in types.indices) { val symType = translate(typeRegistry.genericTypeId(addr, i)) + if (types[i].leastCommonType.isJavaLangObject()) { continue } + val possibleBaseTypes = types[i].possibleConcreteTypes.map { it.baseType } val typeConstraint = z3Context.mkOr( @@ -240,8 +242,10 @@ open class Z3TranslatorVisitor( ) }.toTypedArray() ) + constraints += typeConstraint } + z3Context.mkOr( z3Context.mkAnd(*constraints.toTypedArray()), z3Context.mkEq(translate(expr.addr), translate(nullObjectAddr)) @@ -250,11 +254,24 @@ open class Z3TranslatorVisitor( override fun visit(expr: UtIsGenericTypeExpression): Expr = expr.run { val symType = translate(typeRegistry.symTypeId(addr)) + val symNumDimensions = translate(typeRegistry.symNumDimensions(addr)) + val genericSymType = translate(typeRegistry.genericTypeId(baseAddr, parameterTypeIndex)) - z3Context.mkOr( + val genericNumDimensions = translate(typeRegistry.genericNumDimensions(baseAddr, parameterTypeIndex)) + + val dimensionsConstraint = z3Context.mkEq(symNumDimensions, genericNumDimensions) + + val equalTypeConstraint = z3Context.mkAnd( z3Context.mkEq(symType, genericSymType), + dimensionsConstraint + ) + + val typeConstraint = z3Context.mkOr( + equalTypeConstraint, z3Context.mkEq(translate(expr.addr), translate(nullObjectAddr)) ) + + z3Context.mkAnd(typeConstraint, dimensionsConstraint) } override fun visit(expr: UtEqGenericTypeParametersExpression): Expr = expr.run { @@ -263,6 +280,10 @@ open class Z3TranslatorVisitor( val firstSymType = translate(typeRegistry.genericTypeId(firstAddr, i)) val secondSymType = translate(typeRegistry.genericTypeId(secondAddr, j)) constraints += z3Context.mkEq(firstSymType, secondSymType) + + val firstSymNumDimensions = translate(typeRegistry.genericNumDimensions(firstAddr, i)) + val secondSymNumDimensions = translate(typeRegistry.genericNumDimensions(secondAddr, j)) + constraints += z3Context.mkEq(firstSymNumDimensions, secondSymNumDimensions) } z3Context.mkAnd(*constraints.toTypedArray()) } diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BasePathSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BasePathSelector.kt index 2d3110908a..0e400f527a 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BasePathSelector.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/BasePathSelector.kt @@ -1,10 +1,11 @@ package org.utbot.engine.selectors import org.utbot.engine.ExecutionState +import org.utbot.engine.isPreconditionCheckMethod import org.utbot.engine.pathLogger -import org.utbot.engine.pc.UtSolverStatusUNSAT import org.utbot.engine.pc.UtSolver import org.utbot.engine.pc.UtSolverStatusKind.SAT +import org.utbot.engine.pc.UtSolverStatusUNSAT import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy import org.utbot.framework.UtSettings @@ -59,7 +60,8 @@ abstract class BasePathSelector( /** * @return true if [utSolver] constraints are satisfiable */ - private fun checkUnsat(utSolver: UtSolver): Boolean = utSolver.assertions.isNotEmpty() && utSolver.check(respectSoft = false).statusKind != SAT + private fun checkUnsat(utSolver: UtSolver): Boolean = + utSolver.assertions.isNotEmpty() && utSolver.check(respectSoft = false).statusKind != SAT /** * check fast unsat on forks @@ -82,6 +84,19 @@ abstract class BasePathSelector( state.close() continue } + + // If we have failed assumes, we try to execute the state concretely + if (state.solver.failedAssumptions.isNotEmpty()) { + // But we do not want to execute concretely states because of + // controversies during `preconditionCheck` analysis + if (state.lastMethod?.isPreconditionCheckMethod == true) { + state.close() + } else { + statesForConcreteExecution += state + } + continue + } + return state } return null diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelector.kt new file mode 100644 index 0000000000..c4f37c2924 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelector.kt @@ -0,0 +1,73 @@ +package org.utbot.engine.selectors + +import org.utbot.analytics.EngineAnalyticsContext +import org.utbot.analytics.Predictors +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.selectors.nurs.GreedySearch +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.GeneratedTestCountingStatistics +import org.utbot.engine.selectors.strategies.StoppingStrategy + +/** + * @see Learch + * + * Calculates reward using neural network, when state is offered, and then peeks state with maximum reward + * + * @see choosingStrategy [ChossingStrategy] for [GreedySearch] + * + * [GreedySearch] + */ +abstract class NNRewardGuidedSelector( + protected val generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42, + graph: InterProceduralUnitGraph +) : GreedySearch(choosingStrategy, stoppingStrategy, seed) { + protected val featureExtractor = EngineAnalyticsContext.featureExtractorFactory(graph) + + override val name: String + get() = "NNRewardGuidedSelector" +} + +/** + * Calculate weight of execution state only when it is offered. It has advantage, because it works faster, + * than with recalculation but disadvantage is that some features of execution state can change. + */ +class NNRewardGuidedSelectorWithoutWeightsRecalculation( + generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42, + graph: InterProceduralUnitGraph +) : NNRewardGuidedSelector(generatedTestCountingStatistics, choosingStrategy, stoppingStrategy, seed, graph) { + override fun offerImpl(state: ExecutionState) { + super.offerImpl(state) + featureExtractor.extractFeatures(state, generatedTestCountingStatistics.generatedTestsCount) + } + + override val ExecutionState.weight: Double + get() { + reward = reward ?: Predictors.stateRewardPredictor.predict(features) + return reward as Double + } +} + +/** + * Calculate weight of execution state every time when it needed. It works slower, + * than without recalculation but features are always relevant + */ +class NNRewardGuidedSelectorWithWeightsRecalculation( + generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42, + graph: InterProceduralUnitGraph +) : NNRewardGuidedSelector(generatedTestCountingStatistics, choosingStrategy, stoppingStrategy, seed, graph) { + override val ExecutionState.weight: Double + get() { + featureExtractor.extractFeatures(this, generatedTestCountingStatistics.generatedTestsCount) + return Predictors.stateRewardPredictor.predict(features) + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelectorFactory.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelectorFactory.kt new file mode 100644 index 0000000000..cad9632381 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/NNRewardGuidedSelectorFactory.kt @@ -0,0 +1,49 @@ +package org.utbot.engine.selectors + +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.GeneratedTestCountingStatistics +import org.utbot.engine.selectors.strategies.StoppingStrategy + +/** + * Creates [NNRewardGuidedSelector] + */ +interface NNRewardGuidedSelectorFactory { + operator fun invoke( + generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42, + graph: InterProceduralUnitGraph + ): NNRewardGuidedSelector +} + +/** + * Creates [NNRewardGuidedSelectorWithWeightsRecalculation] + */ +class NNRewardGuidedSelectorWithRecalculationFactory : NNRewardGuidedSelectorFactory { + override fun invoke( + generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int, + graph: InterProceduralUnitGraph + ): NNRewardGuidedSelector = NNRewardGuidedSelectorWithWeightsRecalculation( + generatedTestCountingStatistics, choosingStrategy, stoppingStrategy, seed, graph + ) +} + +/** + * Creates [NNRewardGuidedSelectorWithoutWeightsRecalculation] + */ +class NNRewardGuidedSelectorWithoutRecalculationFactory : NNRewardGuidedSelectorFactory { + override fun invoke( + generatedTestCountingStatistics: GeneratedTestCountingStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int, + graph: InterProceduralUnitGraph + ): NNRewardGuidedSelector = NNRewardGuidedSelectorWithoutWeightsRecalculation( + generatedTestCountingStatistics, choosingStrategy, stoppingStrategy, seed, graph + ) +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelectorBuilder.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelectorBuilder.kt index 8647995743..1d5fb681d8 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelectorBuilder.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/PathSelectorBuilder.kt @@ -2,22 +2,29 @@ package org.utbot.engine.selectors +import org.utbot.analytics.EngineAnalyticsContext import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.TypeRegistry import org.utbot.engine.selectors.StrategyOption.DISTANCE import org.utbot.engine.selectors.StrategyOption.VISIT_COUNTING +import org.utbot.engine.selectors.nurs.CPInstSelector import org.utbot.engine.selectors.nurs.CoveredNewSelector import org.utbot.engine.selectors.nurs.DepthSelector +import org.utbot.engine.selectors.nurs.ForkDepthSelector +import org.utbot.engine.selectors.nurs.InheritorsSelector import org.utbot.engine.selectors.nurs.MinimalDistanceToUncovered import org.utbot.engine.selectors.nurs.NeuroSatSelector -import org.utbot.engine.selectors.nurs.InheritorsSelector import org.utbot.engine.selectors.nurs.RPSelector +import org.utbot.engine.selectors.nurs.SubpathGuidedSelector import org.utbot.engine.selectors.nurs.VisitCountingSelector import org.utbot.engine.selectors.strategies.ChoosingStrategy import org.utbot.engine.selectors.strategies.DistanceStatistics import org.utbot.engine.selectors.strategies.EdgeVisitCountingStatistics +import org.utbot.engine.selectors.strategies.GeneratedTestCountingStatistics +import org.utbot.engine.selectors.strategies.StatementsStatistics import org.utbot.engine.selectors.strategies.StepsLimitStoppingStrategy import org.utbot.engine.selectors.strategies.StoppingStrategy +import org.utbot.engine.selectors.strategies.SubpathStatistics import org.utbot.framework.UtSettings.seedInPathSelector /** @@ -57,6 +64,34 @@ fun inheritorsSelector( builder: InheritorsSelectorBuilder.() -> (Unit) ) = InheritorsSelectorBuilder(graph, typeRegistry).apply(builder).build() + +/** + * build [SubpathGuidedSelector] using [SubpathGuidedSelectorBuilder] + */ +fun subpathGuidedSelector( + graph: InterProceduralUnitGraph, + strategy: StrategyOption, + builder: SubpathGuidedSelectorBuilder.() -> (Unit) +) = SubpathGuidedSelectorBuilder(graph, strategy).apply(builder).build() + +/** + * build [CPInstSelector] using [CPInstSelectorBuilder] + */ +fun cpInstSelector( + graph: InterProceduralUnitGraph, + strategy: StrategyOption, + builder: CPInstSelectorBuilder.() -> (Unit) +) = CPInstSelectorBuilder(graph, strategy).apply(builder).build() + +/** + * build [ForkDepthSelector] using [ForkDepthSelectorBuilder] + */ +fun forkDepthSelector( + graph: InterProceduralUnitGraph, + strategy: StrategyOption, + builder: ForkDepthSelectorBuilder.() -> (Unit) +) = ForkDepthSelectorBuilder(graph, strategy).apply(builder).build() + /** * build [DepthSelector] using [DepthSelectorBuilder] */ @@ -133,10 +168,22 @@ fun visitCountingSelector( fun interleavedSelector(graph: InterProceduralUnitGraph, builder: InterleavedSelectorBuilder.() -> (Unit)) = InterleavedSelectorBuilder(graph).apply(builder).build() +/** + * build [NNRewardGuidedSelector] using [NNRewardGuidedSelectorBuilder] + */ +fun nnRewardGuidedSelector( + graph: InterProceduralUnitGraph, + strategy: StrategyOption, + builder: NNRewardGuidedSelectorBuilder.() -> Unit +) = NNRewardGuidedSelectorBuilder(graph, strategy).apply(builder).build() + data class PathSelectorContext( val graph: InterProceduralUnitGraph, var distanceStatistics: DistanceStatistics? = null, + var subpathStatistics: SubpathStatistics? = null, + var statementsStatistics: StatementsStatistics? = null, var countingStatistics: EdgeVisitCountingStatistics? = null, + var generatedTestCountingStatistics: GeneratedTestCountingStatistics? = null, var stoppingStrategy: StoppingStrategy? = null, ) @@ -192,6 +239,59 @@ class InheritorsSelectorBuilder internal constructor( ) } +/** + * Builder for [SubpathGuidedSelector]. Used in [subpathGuidedSelector] + */ +class SubpathGuidedSelectorBuilder internal constructor( + graph: InterProceduralUnitGraph, + private val strategy: StrategyOption, + context: PathSelectorContext = PathSelectorContext(graph) +) : PathSelectorBuilder(graph, context) { + val seed: Int? = seedInPathSelector + + override fun build() = SubpathGuidedSelector( + withSubpathStatistics(), + withChoosingStrategy(strategy), + requireNotNull(context.stoppingStrategy) { "StoppingStrategy isn't specified" }, + seed!! + ) +} + +/** + * Builder for [CPInstSelectorBuilder]. Used in [cpInstSelector] + */ +class CPInstSelectorBuilder internal constructor( + graph: InterProceduralUnitGraph, + private val strategy: StrategyOption, + context: PathSelectorContext = PathSelectorContext(graph) +) : PathSelectorBuilder(graph, context) { + val seed: Int = 42 + + override fun build() = CPInstSelector( + withStatementsStatistics(), + withChoosingStrategy(strategy), + requireNotNull(context.stoppingStrategy) { "StoppingStrategy isn't specified" }, + seed + ) +} + +/** + * Builder for [ForkDepthSelector]. Used in [forkDepthSelector] + */ +class ForkDepthSelectorBuilder internal constructor( + graph: InterProceduralUnitGraph, + val strategy: StrategyOption, + context: PathSelectorContext = PathSelectorContext(graph) +) : PathSelectorBuilder(graph, context) { + val seed: Int = 42 + + override fun build() = ForkDepthSelector( + withChoosingStrategy(strategy), + requireNotNull(context.stoppingStrategy) { "StoppingStrategy isn't specified" }, + seed + ) +} + /** * Builder for [DepthSelector]. Used in [depthSelector] * @@ -424,6 +524,24 @@ class InterleavedSelectorBuilder internal constructor( override fun build() = InterleavedSelector(builders.map { it.build() }) } +/** + * Builder for [NNRewardGuidedSelector]. Used in [] + */ +class NNRewardGuidedSelectorBuilder internal constructor( + graph: InterProceduralUnitGraph, + private val strategy: StrategyOption, + context: PathSelectorContext = PathSelectorContext(graph), +) : PathSelectorBuilder(graph, context) { + private val seed = seedInPathSelector + override fun build() = EngineAnalyticsContext.nnRewardGuidedSelectorFactory( + withGeneratedTestCountingStatistics(), + withChoosingStrategy(strategy), + requireNotNull(context.stoppingStrategy) { "StoppingStrategy isn't specified" }, + seed!!, + graph + ) +} + /** * Base pathSelectorBuilder that maintains context to attach necessary statistics to graph */ @@ -460,6 +578,36 @@ sealed class PathSelectorBuilder( return context.distanceStatistics!! } + /** + * Create new [SubpathStatistics] and attach it to graph or use created one + */ + protected fun withSubpathStatistics(): SubpathStatistics { + if (context.subpathStatistics == null) { + context.subpathStatistics = SubpathStatistics(graph) + } + return context.subpathStatistics!! + } + + /** + * Create new [StatementsStatistics] and attach it to graph or use created one + */ + protected fun withStatementsStatistics(): StatementsStatistics { + if (context.statementsStatistics == null) { + context.statementsStatistics = StatementsStatistics(graph) + } + return context.statementsStatistics!! + } + + /** + * Create new [GeneratedTestCountingStatistics] and attach it to graph or use created one + */ + protected fun withGeneratedTestCountingStatistics(): GeneratedTestCountingStatistics { + if (context.generatedTestCountingStatistics == null) { + context.generatedTestCountingStatistics = GeneratedTestCountingStatistics(graph) + } + return context.generatedTestCountingStatistics!! + } + /** * Create new [EdgeVisitCountingStatistics] and attach ti to graph or use created one */ diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CPInstSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CPInstSelector.kt new file mode 100644 index 0000000000..261dfc2fc2 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/CPInstSelector.kt @@ -0,0 +1,29 @@ +package org.utbot.engine.selectors.nurs + +import org.utbot.engine.ExecutionState +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.StatementsStatistics +import org.utbot.engine.selectors.strategies.StoppingStrategy + +/** + * Execution states are weighted as 1.0 / StatementStatistics.statementsInMethodCount(exState), + * where StatementStatistics.statementsInMethodCount(exState) + * is number of visited instructions in the current function of execution state + * + * @see @see Klee analog + * + * [NonUniformRandomSearch] + */ +class CPInstSelector( + private val statementStatistics: StatementsStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int? = 42 +) : NonUniformRandomSearch(choosingStrategy, stoppingStrategy, seed) { + + override val name: String + get() = "NURS:CPICnt" + + override val ExecutionState.cost: Double + get() = statementStatistics.statementInMethodCount(this).toDouble() +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/ForkDepthSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/ForkDepthSelector.kt new file mode 100644 index 0000000000..7c5e27aaf7 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/ForkDepthSelector.kt @@ -0,0 +1,24 @@ +package org.utbot.engine.selectors.nurs + +import org.utbot.engine.ExecutionState +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.StoppingStrategy + +/** + * Execution states are weighted as 1.0 / (exState.depth + 1), + * where exState.depth is number of forks on the executionState's path + * + * @see [NonUniformRandomSearch] + */ +class ForkDepthSelector( + policy: ChoosingStrategy, + stoppingPolicy: StoppingStrategy, + seed: Int? = 42 +) : NonUniformRandomSearch(policy, stoppingPolicy, seed) { + + override val ExecutionState.cost: Double + get() = depth.toDouble() + + override val name: String + get() = "ForkDepth" +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/GreedySearch.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/GreedySearch.kt new file mode 100644 index 0000000000..b00d67e871 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/GreedySearch.kt @@ -0,0 +1,78 @@ +package org.utbot.engine.selectors.nurs + +import org.utbot.engine.ExecutionState +import org.utbot.engine.selectors.BasePathSelector +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.StoppingStrategy +import org.utbot.engine.selectors.strategies.StrategyObserver +import kotlin.properties.Delegates +import kotlin.random.Random + +/** + * Selects ExecutionState with maximum weight. + * If there are several states with equal maximum weight, than selects random from them. + */ +abstract class GreedySearch( + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42 +) : BasePathSelector(choosingStrategy, stoppingStrategy), StrategyObserver { + private val states = mutableSetOf() + + private val randomGen: Random = Random(seed) + + override fun update() { + // do nothing by default + } + + override fun offerImpl(state: ExecutionState) { + states += state + } + + override fun pollImpl(): ExecutionState? = peekImpl()?.also { remove(it) } + + /** + * Creates set of states with maximum weight and then pick random + */ + override fun peekImpl(): ExecutionState? { + if (isEmpty()) { + return null + } + + val candidates = mutableListOf() + var bestWeight by Delegates.notNull() + + states.forEach { + val weight = it.weight + + if (candidates.isEmpty()) { + bestWeight = weight + candidates += it + } else { + if (bestWeight <= weight) { + if (bestWeight < weight) { + bestWeight = weight + candidates.clear() + } + candidates += it + } + } + } + + return candidates.random(randomGen) + } + + override fun removeImpl(state: ExecutionState): Boolean { + return states.remove(state) + } + + override fun isEmpty() = states.isEmpty() + + override fun close() { + states.forEach { + it.close() + } + } + + protected abstract val ExecutionState.weight: Double +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/SubpathGuidedSelector.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/SubpathGuidedSelector.kt new file mode 100644 index 0000000000..a17e94a6a2 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/nurs/SubpathGuidedSelector.kt @@ -0,0 +1,30 @@ +package org.utbot.engine.selectors.nurs + +import org.utbot.engine.ExecutionState +import org.utbot.engine.selectors.strategies.ChoosingStrategy +import org.utbot.engine.selectors.strategies.StoppingStrategy +import org.utbot.engine.selectors.strategies.SubpathStatistics + +/** + * Peeks state with minimum frequency of relative subpath. + * See https://github.com/eth-sri/learch/blob/master/klee/lib/Core/Searcher.cpp#L738 + * + * @see [GreedySearch] + */ +class SubpathGuidedSelector( + private val subpathStatistics: SubpathStatistics, + choosingStrategy: ChoosingStrategy, + stoppingStrategy: StoppingStrategy, + seed: Int = 42 +) : GreedySearch(choosingStrategy, stoppingStrategy, seed) { + + + override val name + get() = "NURS:SubpathGuidedSearch" + + /** + * Use - to find minimum + */ + override val ExecutionState.weight: Double + get() = -subpathStatistics.subpathCount(this).toDouble() +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/DistanceStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/DistanceStatistics.kt index e3981c6546..6621a517d2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/DistanceStatistics.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/DistanceStatistics.kt @@ -4,6 +4,7 @@ import org.utbot.engine.Edge import org.utbot.engine.ExecutionState import org.utbot.engine.InterProceduralUnitGraph import org.utbot.engine.isReturn +import org.utbot.engine.pathLogger import org.utbot.engine.stmts import kotlin.math.min import kotlinx.collections.immutable.PersistentList @@ -35,11 +36,20 @@ class DistanceStatistics( graph.stmts.associateWithTo(mutableMapOf()) { 0 } /** - * Drops executionState if all the edges on path are covered and - * there is no reachable uncovered statement + * Drops executionState if all the edges on path are covered (with respect to uncovered + * throw statements of the methods they belong to) and there is no reachable and uncovered statement. */ - override fun shouldDrop(state: ExecutionState) = - state.edges.all { graph.isCovered(it) } && distanceToUncovered(state) == Int.MAX_VALUE + override fun shouldDrop(state: ExecutionState): Boolean { + val shouldDrop = state.edges.all { graph.isCoveredWithAllThrowStatements(it) } && distanceToUncovered(state) == Int.MAX_VALUE + + if (shouldDrop) { + pathLogger.debug { + "Dropping state (lastStatus=${state.solver.lastStatus}) by the distance statistics. MD5: ${state.md5()}" + } + } + + return shouldDrop + } fun isCovered(edge: Edge): Boolean = graph.isCovered(edge) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/EdgeVisitCountingStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/EdgeVisitCountingStatistics.kt index 25384d60fc..1a0ad21348 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/EdgeVisitCountingStatistics.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/EdgeVisitCountingStatistics.kt @@ -3,6 +3,7 @@ package org.utbot.engine.selectors.strategies import org.utbot.engine.Edge import org.utbot.engine.ExecutionState import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.pathLogger import soot.jimple.Stmt import soot.jimple.internal.JReturnStmt import soot.jimple.internal.JReturnVoidStmt @@ -31,7 +32,16 @@ class EdgeVisitCountingStatistics( * - all statements are already covered and execution is complete */ override fun shouldDrop(state: ExecutionState): Boolean { - return state.edges.all { graph.isCovered(it) } && state.isComplete() + val shouldDrop = state.edges.all { graph.isCoveredWithAllThrowStatements(it) } && state.isComplete() + + if (shouldDrop) { + pathLogger.debug { + "Dropping state (lastStatus=${state.solver.lastStatus}) " + + "by the edge visit counting statistics. MD5: ${state.md5()}" + } + } + + return shouldDrop } /** diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GeneratedTestCountingStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GeneratedTestCountingStatistics.kt new file mode 100644 index 0000000000..315673e042 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GeneratedTestCountingStatistics.kt @@ -0,0 +1,15 @@ +package org.utbot.engine.selectors.strategies + +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph + +class GeneratedTestCountingStatistics( + graph: InterProceduralUnitGraph +) : TraverseGraphStatistics(graph) { + var generatedTestsCount = 0 + private set + + override fun onTraversed(executionState: ExecutionState) { + generatedTestsCount++ + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GraphViz.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GraphViz.kt index 34db525a66..057fc70977 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GraphViz.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/GraphViz.kt @@ -15,6 +15,8 @@ import java.nio.file.Files import java.nio.file.Paths import mu.KotlinLogging import org.apache.commons.io.FileUtils +import org.utbot.engine.isLibraryNonOverriddenClass +import org.utbot.engine.isOverridden import soot.jimple.Stmt import soot.toolkits.graph.ExceptionalUnitGraph @@ -27,8 +29,9 @@ class GraphViz( ) : TraverseGraphStatistics(globalGraph) { // Files - private val path = Files.createTempDirectory("Graph-vis") - private val graphJs = Paths.get(path.toString(), "graph.js").toFile() + private val graphVisDirectory = Files.createTempDirectory("Graph-vis") + private val graphVisPathString = graphVisDirectory.toString() + private val graphJs = Paths.get(graphVisPathString, "graph.js").toFile() // Subgraph data private val stmtToSubgraph = mutableMapOf() @@ -40,13 +43,17 @@ class GraphViz( init { // Prepare workspace - for (file in arrayOf( + val requiredFileNames = arrayOf( "full.render.js", "graph.js", "legend.png", "private_method.png", "public_method.png", "render_graph.js", "UseVisJs.html", "viz.js" - )) { + ) + + val classLoader = GraphViz::class.java.classLoader + + for (file in requiredFileNames) { FileUtils.copyInputStreamToFile( - GraphViz::class.java.classLoader.getResourceAsStream("html/$file"), - Paths.get(path.toString(), file).toFile() + classLoader.getResourceAsStream("html/$file"), + Paths.get(graphVisDirectory.toString(), file).toFile() ) } FileWriter(graphJs).use { @@ -58,7 +65,9 @@ class GraphViz( } update() - val path = Paths.get(path.toString(), "UseVisJs.html") + + val path = Paths.get(graphVisPathString, "UseVisJs.html") + logger.debug { "Debug visualization: $path" } if (copyVisualizationPathToClipboard) { @@ -70,72 +79,71 @@ class GraphViz( override fun onVisit(executionState: ExecutionState) { var src: Stmt = executionState.path.firstOrNull() ?: return + update() - val uncompletedStack = mutableListOf(DotGraph(stmtToSubgraph[src]!!, 0)) // Only uncompleted methods execution - val fullStack = mutableListOf(DotGraph(stmtToSubgraph[src]!!, 0)) + val subgraph = stmtToSubgraph[src] ?: error("No subgraph found for the $src statement") + + val uncompletedStack = mutableListOf(DotGraph(subgraph, 0)) // Only uncompleted methods execution + val fullStack = mutableListOf(DotGraph(subgraph, 0)) val dotGlobalGraph = DotGraph("GlobalGraph", 0) // Add edges to dot graph - subgraphEdges[stmtToSubgraph[src]!!]?.forEach { + subgraphEdges[subgraph]?.forEach { uncompletedStack.last().addDotEdge(it) fullStack.last().addDotEdge(it) } - (graph.registeredEdges + graph.implicitEdges).forEach { - if (!libraryGraphs.contains(stmtToSubgraph[it.src]) && !libraryGraphs.contains(stmtToSubgraph[it.src])) { - dotGlobalGraph.addDotEdge(it) + + graph.allEdges.forEach { edge -> + val (edgeSrc, edgeDst, _) = edge + + if (stmtToSubgraph[edgeSrc] !in libraryGraphs && stmtToSubgraph[edgeDst] !in libraryGraphs) { + dotGlobalGraph.addDotEdge(edge) } } // Split execution stack - for (dst in executionState.path + executionState.stmt) { + val allStatements = executionState.path + executionState.stmt + + for (dst in allStatements) { val edge = executionState.edges.findLast { it.src == src && it.dst == dst } ?: executionState.lastEdge - if (edge == null || edge.src != src || edge.dst != dst) continue - val graph = stmtToSubgraph[dst]!! - uncompletedStack.last().dotNode(src)?.visited = true - uncompletedStack.last().dotNode(dst)?.visited = true - uncompletedStack.last().dotEdge(edge)?.isVisited = true + if (edge == null || edge.src != src || edge.dst != dst) { + continue + } + + val graphName = stmtToSubgraph[dst] ?: error("No subgraph found for the $dst statement") - fullStack.last().dotNode(src)?.visited = true - fullStack.last().dotNode(dst)?.visited = true - fullStack.last().dotEdge(edge)?.isVisited = true - dotGlobalGraph.dotNode(src)?.visited = true - dotGlobalGraph.dotNode(dst)?.visited = true - dotGlobalGraph.dotEdge(edge)?.isVisited = true + uncompletedStack.last().markAsVisited(src, dst, edge) + fullStack.last().markAsVisited(src, dst, edge) + dotGlobalGraph.markAsVisited(src, dst, edge) // Check if we returned to previous method - if (uncompletedStack.last().name != graph) { - if (uncompletedStack.size > 1 && uncompletedStack[uncompletedStack.size - 2].name == graph) { + if (uncompletedStack.last().name != graphName) { + if (uncompletedStack.size > 1 && uncompletedStack[uncompletedStack.size - 2].name == graphName) { uncompletedStack.removeLast() } else { - uncompletedStack += DotGraph(graph, uncompletedStack.size) - subgraphEdges[graph]?.forEach { - uncompletedStack.last().addDotEdge(it) - } + uncompletedStack.addGraphWithEdges(graphName, uncompletedStack.size) } } // Check if we change execution method - if (graph != stmtToSubgraph[src]) { - fullStack += DotGraph(graph, uncompletedStack.size - 1) - subgraphEdges[graph]?.forEach { - fullStack.last().addDotEdge(it) - } + if (graphName != stmtToSubgraph[src]) { + fullStack.addGraphWithEdges(graphName, uncompletedStack.size - 1) } src = dst } // Filter library methods - uncompletedStack.removeIf { libraryGraphs.contains(it.name) } - fullStack.removeIf { libraryGraphs.contains(it.name) } + uncompletedStack.removeIf { it.name in libraryGraphs } + fullStack.removeIf { it.name in libraryGraphs } // Update nodes and edges properties - updateProperties(dotGlobalGraph, executionState) - uncompletedStack.forEach { updateProperties(it, executionState) } - fullStack.forEach { updateProperties(it, executionState) } + dotGlobalGraph.updateProperties(executionState) + uncompletedStack.forEach { it.updateProperties(executionState) } + fullStack.forEach { it.updateProperties(executionState) } // Write result FileWriter(graphJs).use { writer -> @@ -151,57 +159,89 @@ class GraphViz( } } + private fun MutableList.addGraphWithEdges(graphName: String, depth: Int) { + this += DotGraph(graphName, depth) + subgraphEdges[graphName]?.forEach { + last().addDotEdge(it) + } + } + + private fun DotGraph.markAsVisited(src: Stmt, dst: Stmt, edge: Edge) { + dotNode(src)?.visited = true + dotNode(dst)?.visited = true + dotEdge(edge)?.isVisited = true + } + + // Note that we do not use `shouldRegister` here, because visualization + // does not depend on the fact of registration. Otherwise, we'd + // lose overridden classes here and don't join them at the visualization. override fun onJoin(stmt: Stmt, graph: ExceptionalUnitGraph, shouldRegister: Boolean) { - if (!shouldRegister) { - libraryGraphs.add(graph.body?.method?.declaringClass?.shortName + "." + graph.body?.method?.name) + val method = graph.body?.method + val declaringClass = method?.declaringClass + + if (declaringClass?.isLibraryNonOverriddenClass == true) { + libraryGraphs += declaringClass.shortName + "." + method.name } + update() } override fun onTraversed(executionState: ExecutionState) { - if (executionState.exception != null) - exceptionalEdges[executionState.lastEdge ?: return] = executionState.exception.concrete.toString() + if (executionState.lastEdge == null) { + return + } + + if (executionState.exception != null) { + exceptionalEdges[executionState.lastEdge] = executionState.exception.concrete.toString() + } } private fun update() { graph.graphs.forEachIndexed { _, graph -> - val declaringClassName = - if (graph.body.method.isDeclared) graph.body?.method?.declaringClass?.shortName else "UnknownClass" - val name = declaringClassName + "." + graph.body?.method?.name - subgraphVisibility.putIfAbsent(name, if (graph.body?.method?.isPrivate == true) "private" else "public") - subgraphEdges.putIfAbsent(name, mutableSetOf()) + val method = graph.body?.method + val declaringClass = if (method?.isDeclared == true) method.declaringClass?.shortName else "UnknownClass" + val methodName = declaringClass + "." + method?.name + val visibility = if (method?.isPrivate == true) "private" else "public" + + subgraphVisibility.putIfAbsent(methodName, visibility) + subgraphEdges.putIfAbsent(methodName, mutableSetOf()) + graph.stmts.forEach { stmt -> - stmtToSubgraph[stmt] = name + stmtToSubgraph[stmt] = methodName } } - for (edge in graph.registeredEdges + graph.implicitEdges) { - subgraphEdges[stmtToSubgraph[edge.src]!!]!!.add(edge) + for (edge in graph.allEdges) { + val subgraph = stmtToSubgraph[edge.src] ?: error("No subgraph found for the ${edge.src} statement") + val edges = subgraphEdges[subgraph] ?: error("No subgraph edges found for the $subgraph") + edges += edge } } - private fun updateProperties(graph: DotGraph, executionState: ExecutionState) { + private fun DotGraph.updateProperties(executionState: ExecutionState) { // Node property: Last execution state - graph.dotNode(executionState.stmt)?.isLast = true + dotNode(executionState.stmt)?.isLast = true // Node property: covered - globalGraph.stmts.forEach { graph.dotNode(it)?.covered = globalGraph.isCovered(it) } + globalGraph.stmts.forEach { dotNode(it)?.covered = globalGraph.isCoveredIgnoringRegistration(it) } + + val queue = pathSelector.queue() // Node property: In queue - pathSelector.queue().forEach { graph.dotNode(it.first.stmt)?.inQueue = true } + queue.forEach { (state, _) -> dotNode(state.stmt)?.inQueue = true } // Node property: Head of queue - if (!pathSelector.isEmpty()) graph.dotNode(pathSelector.peek()!!.stmt)?.headQueue = true + pathSelector.peek()?.let { dotNode(it.stmt)?.headQueue = true } // Edge property: covered - (globalGraph.registeredEdges + globalGraph.implicitEdges).forEach { - graph.dotEdge(it)?.isCovered = globalGraph.isCovered(it) + globalGraph.allEdges.forEach { + dotEdge(it)?.isCovered = globalGraph.isCovered(it) } // Edge property: Edge to queue stmt property - pathSelector.queue().filter { it.first.lastEdge != null && it.first.lastEdge !in globalGraph.implicitEdges } + queue.filter { (state, _) -> state.lastEdge != null && state.lastEdge !in globalGraph.implicitEdges } .forEach { (state, weight) -> - graph.dotEdge(state.lastEdge!!)?.apply { + dotEdge(state.lastEdge!!)?.apply { isToQueueStmt = true label = "%.2f".format(weight) } @@ -209,15 +249,14 @@ class GraphViz( // Edge property: implicit edge to exception exceptionalEdges.keys.forEach { - graph.dotEdge(it)?.apply { + dotEdge(it)?.apply { isExceptional = true - toException = exceptionalEdges[it]!! + toException = exceptionalEdges.getValue(it) } } } } - /** * Represents graph node in dot format */ @@ -229,7 +268,6 @@ data class DotNode(val id: Int, val label: String) { var visited: Boolean = false var invoke: Boolean = false var isLast: Boolean = false - var isImplicit: Boolean = false private var toolTip: String = "" private val fillColor: String @@ -241,17 +279,17 @@ data class DotNode(val id: Int, val label: String) { return mixtureColors(colors, "white") } - val color: String + private val color: String get() = if (visited) "red" else "black" - val shape: String + private val shape: String get() = when { returned -> "circle" invoke -> "cds" else -> "rectangle" } - val width: String + private val width: String get() = when { isLast -> "5.0" invoke -> "2.0" @@ -274,7 +312,7 @@ data class DotEdge(val id: Int, val srcId: Int, val dstId: Int, var label: Strin var isCaller = false var toException = "" - val color: String + private val color: String get() { val colors = mutableListOf() if (isVisited) colors += "red" @@ -283,7 +321,7 @@ data class DotEdge(val id: Int, val srcId: Int, val dstId: Int, var label: Strin return mixtureColors(colors, "black") } - val style: String + private val style: String get() = when { isCaller -> "dotted" isExceptional -> "dashed" @@ -300,30 +338,29 @@ data class DotEdge(val id: Int, val srcId: Int, val dstId: Int, var label: Strin * Represents graph in dot format */ class DotGraph(val name: String, val depth: Int) { - var nodeId = 0 - var edgeId = 0 - val nodes = mutableMapOf() + private var nodeId = 0 + private var edgeId = 0 + private val nodes = mutableMapOf() val edges = mutableMapOf() fun dotNode(stmt: Stmt): DotNode? = nodes[stmt] - fun dotEdge(edge: Edge): DotEdge? { - return edges[edge] - } + fun dotEdge(edge: Edge): DotEdge? = edges[edge] - fun addDotEdge(edge: Edge): DotEdge { - val srcNode = nodes.getOrPut(edge.src) { DotNode(nodeId++, edge.src.toDotName()) } - val dstNode = nodes.getOrPut(edge.dst) { DotNode(nodeId++, edge.dst.toDotName()) } + fun addDotEdge(edge: Edge): DotEdge = with(edge) { + val srcNode = nodes.getOrPut(src) { DotNode(nodeId++, src.toDotName()) } + val dstNode = nodes.getOrPut(dst) { DotNode(nodeId++, dst.toDotName()) } - srcNode.returned = edge.src.isReturn - srcNode.invoke = edge.src.containsInvokeExpr() - dstNode.returned = edge.dst.isReturn - dstNode.invoke = edge.dst.containsInvokeExpr() + srcNode.returned = src.isReturn + srcNode.invoke = src.containsInvokeExpr() + + dstNode.returned = dst.isReturn + dstNode.invoke = dst.containsInvokeExpr() val dotEdge = edges.getOrPut(edge) { DotEdge(edgeId++, srcNode.id, dstNode.id) } - dotEdge.isCaller = edge.decisionNum == CALL_DECISION_NUM + dotEdge.isCaller = decisionNum == CALL_DECISION_NUM - return dotEdge + dotEdge } // Remove special characters in dot format @@ -347,25 +384,27 @@ class DotGraph(val name: String, val depth: Int) { edges.filter { !it.value.isExceptional }.forEach { append(it.value.toString()) } // Add implicit edge and node (exception case) - edges.values.filter { it.isExceptional }.forEach { - val implicitId = nodeId++ - val color = if (it.isCovered) "azure3" else "black" - val shortName = it.toException.split(":")[0].filter { it2 -> it2.isUpperCase() } - - if (it.isCovered) { - append( - "\"$implicitId\" [label=\"${shortName}\",tooltip=\"${it.toException}\"," + - "shape=circle,style=\"filled,dashed\",fillcolor=azure3];\n" - ) - } else { - append( - "\"$implicitId\" [label=\"${shortName}\", tooltip=\"${it.toException}\"," + - "shape=circle, style=dashed];\n" - ) - } + edges.values + .filter { it.isExceptional } + .forEach { + val implicitId = nodeId++ + val color = if (it.isCovered) "azure3" else "black" + val shortName = it.toException.split(":")[0].filter { it2 -> it2.isUpperCase() } + + if (it.isCovered) { + append( + "\"$implicitId\" [label=\"${shortName}\",tooltip=\"${it.toException}\"," + + "shape=circle,style=\"filled,dashed\",fillcolor=azure3];\n" + ) + } else { + append( + "\"$implicitId\" [label=\"${shortName}\", tooltip=\"${it.toException}\"," + + "shape=circle, style=dashed];\n" + ) + } - append("\"${it.srcId}\" -> \"$implicitId\" [style=dashed,color=\"$color\"];\n") - } + append("\"${it.srcId}\" -> \"$implicitId\" [style=dashed,color=\"$color\"];\n") + } // Close dot graph append("}") diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StatementsStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StatementsStatistics.kt new file mode 100644 index 0000000000..375462191c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StatementsStatistics.kt @@ -0,0 +1,37 @@ +package org.utbot.engine.selectors.strategies + +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph +import soot.SootMethod +import soot.jimple.Stmt + +/** + * Calculates + * - number of times for which state’s current instruction has been visited in [statementsCount] + * - number of instructions visited in state’s current method in [statementsInMethodCount] + */ +class StatementsStatistics( + graph: InterProceduralUnitGraph +) : TraverseGraphStatistics(graph) { + private val statementsCount = mutableMapOf() + private val statementsInMethodCount = mutableMapOf() + + override fun onVisit(executionState: ExecutionState) { + statementsCount.compute(executionState.stmt) { _, v -> + v?.plus(1) ?: 1 + } + + if (statementsCount[executionState.stmt] == 1) { + executionState.lastMethod?.let { + statementsInMethodCount.compute(it) { _, v -> + v?.plus(1) ?: 1 + } + } + } + } + + fun statementCount(executionState: ExecutionState) = statementsCount.getOrDefault(executionState.stmt, 1) + + fun statementInMethodCount(executionState: ExecutionState) = + executionState.lastMethod?.let { statementsInMethodCount.getOrDefault(it, 1) } ?: 0 +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StepsLimitStoppingStrategy.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StepsLimitStoppingStrategy.kt index 41273ef444..f17d2b6750 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StepsLimitStoppingStrategy.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/StepsLimitStoppingStrategy.kt @@ -3,6 +3,7 @@ package org.utbot.engine.selectors.strategies import org.utbot.engine.Edge import org.utbot.engine.ExecutionState import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.engine.pathLogger /** * Stopping strategy that suggest to stop execution @@ -16,7 +17,13 @@ class StepsLimitStoppingStrategy( private var stepsCounter: Int = 0 override fun shouldStop(): Boolean { - return stepsCounter > stepsLimit + val shouldDrop = stepsCounter > stepsLimit + + if (shouldDrop) { + pathLogger.debug { "Steps limit has been exceeded: current step limit is $stepsLimit" } + } + + return shouldDrop } override fun onVisit(edge: Edge) { diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/SubpathStatistics.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/SubpathStatistics.kt new file mode 100644 index 0000000000..0110cf03c9 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/selectors/strategies/SubpathStatistics.kt @@ -0,0 +1,51 @@ +package org.utbot.engine.selectors.strategies + +import org.utbot.engine.CALL_DECISION_NUM +import org.utbot.engine.Edge +import org.utbot.engine.ExecutionState +import org.utbot.engine.InterProceduralUnitGraph +import org.utbot.framework.UtSettings +import kotlin.math.pow + +/** + * Calculates frequency of subpathes of statements with length equal to 2^{index}. + */ +class SubpathStatistics( + graph: InterProceduralUnitGraph, + index: Int = UtSettings.subpathGuidedSelectorIndex +) : TraverseGraphStatistics(graph) { + private val subpathCount = mutableMapOf, Int>() + private val length: Int = 2.0.pow(index).toInt() + + /** + * Take length last edges from state's path and handle exception edges + */ + private fun ExecutionState.getSubpath(length: Int): List { + val subpath = mutableListOf() + val actualLength = if (pathLength >= length) length else pathLength + + var current = stmt + var exceptionNumber = 0 + (0 until actualLength).forEach { + val decision = decisionPath[decisionPath.size - it - 1] + if (decision < CALL_DECISION_NUM) { + exceptionNumber++ + } + val i = path.size - it - 1 + exceptionNumber + val prev = if (i == path.size) stmt else path[i] + subpath += Edge(prev, current, decision) + current = prev + } + + return subpath + } + + override fun onVisit(executionState: ExecutionState) { + subpathCount.compute(executionState.getSubpath(length)) { _, v -> + v?.plus(1) ?: 1 + } + } + + fun subpathCount(executionState: ExecutionState): Int = + subpathCount.getOrPut(executionState.getSubpath(length)) { 1 } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicState.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicState.kt index ea642a2482..cbe2d5c7ef 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicState.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicState.kt @@ -15,10 +15,12 @@ data class SymbolicState( val memory: Memory = Memory(), ) { operator fun plus(update: SymbolicStateUpdate): SymbolicState = - SymbolicState( - solver.add(hard = update.hardConstraints, soft = update.softConstraints), - memory = memory.update(update.memoryUpdates), - ) + with(update) { + SymbolicState( + solver.add(hard = hardConstraints, soft = softConstraints, assumption = assumptions), + memory = memory.update(memoryUpdates), + ) + } operator fun plus(update: HardConstraint): SymbolicState = plus(SymbolicStateUpdate(hardConstraints = update)) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicStateUpdate.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicStateUpdate.kt index 87ef1396e0..0b883d0a76 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicStateUpdate.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/symbolic/SymbolicStateUpdate.kt @@ -39,14 +39,30 @@ class SoftConstraint( SoftConstraint(addConstraints(other.constraints)) } +/** + * Represent constraints that must be satisfied for symbolic execution. + * At the same time, if they don't, the state they belong to still + * might be executed concretely without these assume. + * + * @see + */ +class Assumption( + constraints: Set = emptySet() +): Constraint(constraints) { + override fun plus(other: Assumption): Assumption = Assumption(addConstraints(other.constraints)) + + override fun toString() = constraints.joinToString(System.lineSeparator()) +} + /** * Represents one or more updates that can be applied to [SymbolicState]. * * TODO: move [localMemoryUpdates] to another place */ data class SymbolicStateUpdate( - val hardConstraints: HardConstraint = HardConstraint(setOf()), - val softConstraints: SoftConstraint = SoftConstraint(setOf()), + val hardConstraints: HardConstraint = HardConstraint(), + val softConstraints: SoftConstraint = SoftConstraint(), + val assumptions: Assumption = Assumption(), val memoryUpdates: MemoryUpdate = MemoryUpdate(), val localMemoryUpdates: LocalMemoryUpdate = LocalMemoryUpdate() ) { @@ -54,6 +70,7 @@ data class SymbolicStateUpdate( SymbolicStateUpdate( hardConstraints = hardConstraints + update.hardConstraints, softConstraints = softConstraints + update.softConstraints, + assumptions = assumptions + update.assumptions, memoryUpdates = memoryUpdates + update.memoryUpdates, localMemoryUpdates = localMemoryUpdates + update.localMemoryUpdates ) @@ -64,6 +81,9 @@ data class SymbolicStateUpdate( operator fun plus(update: SoftConstraint): SymbolicStateUpdate = plus(SymbolicStateUpdate(softConstraints = update)) + operator fun plus(update: Assumption): SymbolicStateUpdate = + plus(SymbolicStateUpdate(assumptions = update)) + operator fun plus(update: MemoryUpdate): SymbolicStateUpdate = plus(SymbolicStateUpdate(memoryUpdates = update)) @@ -87,6 +107,10 @@ fun Collection.asSoftConstraint() = SoftConstraint(transformTo fun UtBoolExpression.asSoftConstraint() = SoftConstraint(setOf(this)) +fun Collection.asAssumption() = Assumption(toSet()) + +fun UtBoolExpression.asAssumption() = Assumption(setOf(this)) + private fun Collection.transformToSet(): Set = if (this is Set) this else toSet() fun HardConstraint.asUpdate() = SymbolicStateUpdate(hardConstraints = this) diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceMockListener.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceMockListener.kt new file mode 100644 index 0000000000..756cef4059 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/ForceMockListener.kt @@ -0,0 +1,21 @@ +package org.utbot.engine.util.mockListeners +import org.utbot.engine.EngineController +import org.utbot.engine.MockStrategy +import org.utbot.engine.util.mockListeners.exceptions.ForceMockCancellationException + +/** + * Listener for mocker events in [org.utbot.engine.UtBotSymbolicEngine]. + * If forced mock happened, cancels the engine job. + * + * Supposed to be created only if Mockito is not installed. + */ +class ForceMockListener: MockListener { + var forceMockHappened = false + private set + + override fun onShouldMock(controller: EngineController, strategy: MockStrategy) { + // If force mocking happened -- сancel engine job + controller.job?.cancel(ForceMockCancellationException()) + forceMockHappened = true + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListener.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListener.kt new file mode 100644 index 0000000000..5d004e3a1c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListener.kt @@ -0,0 +1,11 @@ +package org.utbot.engine.util.mockListeners + +import org.utbot.engine.EngineController +import org.utbot.engine.MockStrategy + +/** + * Listener that can be attached using [MockListenerController] to mocker in [org.utbot.engine.UtBotSymbolicEngine]. + */ +interface MockListener { + fun onShouldMock(controller: EngineController, strategy: MockStrategy) +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListenerController.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListenerController.kt new file mode 100644 index 0000000000..765b0edb04 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/MockListenerController.kt @@ -0,0 +1,19 @@ +package org.utbot.engine.util.mockListeners + +import org.utbot.engine.EngineController +import org.utbot.engine.MockStrategy + +/** + * Controller that allows to attach listeners to mocker in [org.utbot.engine.UtBotSymbolicEngine]. + */ +class MockListenerController(private val controller: EngineController) { + val listeners = mutableListOf() + + fun attach(listener: MockListener) { + listeners += listener + } + + fun onShouldMock(strategy: MockStrategy) { + listeners.map { it.onShouldMock(controller, strategy) } + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/exceptions/ForceMockCancellationException.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/exceptions/ForceMockCancellationException.kt new file mode 100644 index 0000000000..d5b2e3d86c --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/mockListeners/exceptions/ForceMockCancellationException.kt @@ -0,0 +1,8 @@ +package org.utbot.engine.util.mockListeners.exceptions + +import kotlinx.coroutines.CancellationException + +/** + * Exception used in [org.utbot.engine.util.mockListeners.ForceMockListener]. + */ +class ForceMockCancellationException: CancellationException("Forced mocks without Mockito") diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/util/statics/concrete/EnumConcreteUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/util/statics/concrete/EnumConcreteUtils.kt new file mode 100644 index 0000000000..84deca8af5 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/util/statics/concrete/EnumConcreteUtils.kt @@ -0,0 +1,245 @@ +package org.utbot.engine.util.statics.concrete + +import org.utbot.common.withAccessibility +import org.utbot.engine.* +import org.utbot.engine.nullObjectAddr +import org.utbot.engine.pc.addrEq +import org.utbot.engine.pc.mkEq +import org.utbot.engine.pc.mkNot +import org.utbot.engine.pc.select +import org.utbot.engine.symbolic.SymbolicStateUpdate +import org.utbot.engine.symbolic.asHardConstraint +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.field +import soot.SootClass +import soot.SootField +import soot.SootMethod +import soot.Type +import soot.Value +import soot.jimple.StaticFieldRef +import soot.jimple.Stmt +import soot.jimple.internal.JAssignStmt + +fun UtBotSymbolicEngine.makeSymbolicValuesFromEnumConcreteValues( + type: Type, + enumConstantRuntimeValues: List> +): Pair, Map> { + val enumConstantResultsWithNames = enumConstantRuntimeValues.map { + it.name to toMethodResult(it, type) + } + val enumConstantSymbolicValuesWithNames = enumConstantResultsWithNames.map { + it.first to (it.second.symbolicResult as SymbolicSuccess).value as ObjectValue + } + + val enumConstantSymbolicValues = enumConstantSymbolicValuesWithNames.map { it.second } + + val enumConstantSymbolicResultsByName = enumConstantResultsWithNames.toMap() + + return enumConstantSymbolicValues to enumConstantSymbolicResultsByName +} + +fun associateEnumSootFieldsWithConcreteValues( + enumFields: List, + enumConstants: List> +): List>> = + enumFields.map { enumSootField -> + val enumField = enumSootField.fieldId.field + + val fieldValues = if (enumSootField.isStatic) { + val staticFieldValue = enumField.withAccessibility { enumField.get(null) } + + listOf(staticFieldValue) + } else { + // extract ordinal, name and other non static fields for every enum constant + enumConstants.map { concreteValue -> + enumField.withAccessibility { enumField.get(concreteValue) } + } + } + + enumSootField to fieldValues + } + +/** + * Construct symbolic updates for enum static fields and a symbolic value for a local in the left part of the assignment. + */ +fun UtBotSymbolicEngine.makeEnumStaticFieldsUpdates( + staticFields: List>>, + declaringClass: SootClass, + enumConstantSymbolicResultsByName: Map, + enumConstantSymbolicValues: List, + enumClassValue: ObjectValue, + fieldId: FieldId +): Pair { + var staticFieldsUpdates = SymbolicStateUpdate() + var symbolicValueForLocal: SymbolicValue? = null + + staticFields.forEach { (sootStaticField, staticFieldRuntimeValue) -> + val fieldName = sootStaticField.name + val fieldType = sootStaticField.type + val (fieldSymbolicResult, fieldSymbolicStateUpdate) = constructEnumStaticFieldResult( + fieldName, + fieldType, + declaringClass, + enumConstantSymbolicResultsByName, + staticFieldRuntimeValue.single(), + enumConstantSymbolicValues + ) + + val fieldSymbolicValue = (fieldSymbolicResult as SymbolicSuccess).value + val fieldValue = valueToExpression(fieldSymbolicValue, fieldType) + + val fieldUpdate = objectUpdate(enumClassValue, sootStaticField, fieldValue) + + staticFieldsUpdates += fieldSymbolicStateUpdate + fieldUpdate + + // enum constant could not be null + if (fieldName in enumConstantSymbolicResultsByName) { + val canBeNull = addrEq(fieldSymbolicValue.addr, nullObjectAddr) + val canNotBeNull = mkNot(canBeNull) + + staticFieldsUpdates += canNotBeNull.asHardConstraint() + } + + // save value to associate it with local if required + if (sootStaticField.name == fieldId.name) { + symbolicValueForLocal = fieldSymbolicValue + } + } + + return staticFieldsUpdates to symbolicValueForLocal +} + +fun UtBotSymbolicEngine.makeEnumNonStaticFieldsUpdates( + enumConstantSymbolicValues: List, + nonStaticFields: List>> +): SymbolicStateUpdate { + var nonStaticFieldsUpdates = SymbolicStateUpdate() + + for ((i, enumConstantSymbolicValue) in enumConstantSymbolicValues.withIndex()) { + nonStaticFields.forEach { (sootNonStaticField, nonStaticFieldRuntimeValues) -> + val nonStaticFieldRuntimeValue = nonStaticFieldRuntimeValues[i] + + val fieldType = sootNonStaticField.type + val (fieldSymbolicResult, fieldSymbolicStateUpdate) = toMethodResult( + nonStaticFieldRuntimeValue, + fieldType + ) + + nonStaticFieldsUpdates += fieldSymbolicStateUpdate + + val fieldSymbolicValue = (fieldSymbolicResult as SymbolicSuccess).value + val fieldValue = valueToExpression(fieldSymbolicValue, fieldType) + + val chunkId = hierarchy.chunkIdForField(enumConstantSymbolicValue.type, sootNonStaticField) + val descriptor = MemoryChunkDescriptor(chunkId, enumConstantSymbolicValue.type, fieldType) + val array = memory.findArray(descriptor) + val arraySelectEqualsValue = mkEq(array.select(enumConstantSymbolicValue.addr), fieldValue) + + nonStaticFieldsUpdates += arraySelectEqualsValue.asHardConstraint() + } + } + + return nonStaticFieldsUpdates +} + +fun isEnumValuesFieldName(fieldName: String): Boolean = fieldName == "\$VALUES" + +/** + * Checks that [this] is enum which affects any external static fields in its / sections. + */ +fun SootClass.isEnumAffectingExternalStatics(typeResolver: TypeResolver): Boolean { + if (!isEnum) { + return false + } + + // enum active body contains invocations so we can check only + val staticInitializer = staticInitializerOrNull() ?: return false + + val implementedInterfaces = typeResolver + .findOrConstructAncestorsIncludingTypes(type) + .filter { type -> type.sootClass.isInterface } + + return staticInitializer.isTouchingExternalStatics(this, mutableSetOf(), implementedInterfaces) +} + +/** + * Returns whether [this] method touches any statics from any types + * except [currentClass] and its interfaces in [currentClassImplementedInterfaces]. + * + * NOTE: see org.utbot.examples.enums.ClassWithEnum.{EnumWithStaticAffectingInit, OuterStaticUsageEnum} for examples. + */ +fun SootMethod.isTouchingExternalStatics( + currentClass: SootClass, + alreadyProcessed: MutableSet, + currentClassImplementedInterfaces: List +): Boolean { + if (this in alreadyProcessed) { + return false + } + + alreadyProcessed += this + + // active body could be missing ((java.lang.String,int)>, for example) + // so consider it as not affecting external statics + if (!canRetrieveBody()) { + return false + } + + return jimpleBody().units.any { + if (it !is Stmt) { + return@any false + } + + when (it) { + is JAssignStmt -> { + val leftOp = it.leftOp + val rightOp = it.rightOp + + val assigningOuterStatics = isExternalStaticField( + leftOp, + currentClassImplementedInterfaces, + currentClass + ) + + val assignmentFromOuterStatics = isExternalStaticField( + rightOp, + currentClassImplementedInterfaces, + currentClass + ) + + assigningOuterStatics || assignmentFromOuterStatics + } + else -> { + if (it.containsInvokeExpr()) { + it.invokeExpr.method.isTouchingExternalStatics( + currentClass, + alreadyProcessed, + currentClassImplementedInterfaces + ) + } else { + false + } + } + } + } +} + +/** + * Determines whether [fieldRef] is static field not from [currentClass] + * (except static fields from all interfaces implemented by [currentClass], stored in [currentClassImplementedInterfaces]). + */ +private fun isExternalStaticField( + fieldRef: Value, + currentClassImplementedInterfaces: List, + currentClass: SootClass +): Boolean { + if (fieldRef !is StaticFieldRef) { + return false + } + + val declaringClass = fieldRef.field.declaringClass + + val classInImplementedInterfaces = declaringClass.type in currentClassImplementedInterfaces + + return !classInImplementedInterfaces && declaringClass != currentClass +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Z3initializer.kt b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Z3initializer.kt index 2696c0bf69..97677c78cd 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Z3initializer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/engine/z3/Z3initializer.kt @@ -2,6 +2,7 @@ package org.utbot.engine.z3 import com.microsoft.z3.Context import com.microsoft.z3.Global +import org.utbot.common.FileUtil import java.io.File import java.nio.file.Files.createTempDirectory @@ -20,27 +21,28 @@ abstract class Z3Initializer : AutoCloseable { companion object { private val libraries = listOf("libz3", "libz3java") private val vcWinLibrariesToLoadBefore = listOf("vcruntime140", "vcruntime140_1") - private const val supportedArch = "amd64" + private val supportedArchs = setOf("amd64", "x86_64") private val initializeCallback by lazy { System.setProperty("z3.skipLibraryLoad", "true") val arch = System.getProperty("os.arch") - require(supportedArch == arch) { "Not supported arch: $arch" } + require(arch in supportedArchs) { "Not supported arch: $arch" } - val osProperty = System.getProperty("os.name") + val osProperty = System.getProperty("os.name").toLowerCase() val (ext, allLibraries) = when { - osProperty.startsWith("Windows") -> ".dll" to vcWinLibrariesToLoadBefore + libraries - osProperty.startsWith("Linux") -> ".so" to libraries + osProperty.startsWith("windows") -> ".dll" to vcWinLibrariesToLoadBefore + libraries + osProperty.startsWith("linux") -> ".so" to libraries + osProperty.startsWith("mac") -> ".dylib" to libraries else -> error("Unknown OS: $osProperty") } val libZ3DllUrl = Z3Initializer::class.java .classLoader .getResource("lib/x64/libz3.dll") ?: error("Can't find native library folder") - //can't take resource of parent folder right here because in obfuscated jar parent folder + // can't take resource of parent folder right here because in obfuscated jar parent folder // can be missed (e.g., in case if obfuscation was applied) val libFolder: String? if (libZ3DllUrl.toURI().scheme == "jar") { - val tempDir = createTempDirectory(null).toFile().apply { deleteOnExit() } + val tempDir = FileUtil.createTempDirectory("libs-").toFile() allLibraries.forEach { name -> Z3Initializer::class.java diff --git a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt index 107c357cc1..b87f8dcb1e 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/external/api/UtBotJavaApi.kt @@ -12,15 +12,26 @@ import org.utbot.framework.codegen.model.ModelBasedCodeGeneratorService import org.utbot.framework.concrete.UtConcreteExecutionData import org.utbot.framework.concrete.UtConcreteExecutionResult import org.utbot.framework.concrete.UtExecutionInstrumentation +import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtBotTestCaseGenerator import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtMethod +import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.UtTestCase import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.primitiveByWrapper +import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.framework.plugin.api.util.wrapperByPrimitive +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider import org.utbot.instrumentation.ConcreteExecutor import org.utbot.instrumentation.execute import java.lang.reflect.Method @@ -91,6 +102,11 @@ object UtBotJavaApi { } } + /** + * Generates test cases using default workflow. + * + * @see [fuzzingTestCases] + */ @JvmStatic @JvmOverloads fun generateTestCases( @@ -128,6 +144,73 @@ object UtBotJavaApi { return testCases } + /** + * Generates test cases using only fuzzing workflow. + * + * @see [generateTestCases] + */ + @JvmStatic + @JvmOverloads + fun fuzzingTestCases( + methodsForAutomaticGeneration: List, + classUnderTest: Class<*>, + classpath: String, + dependencyClassPath: String, + mockStrategyApi: MockStrategyApi = MockStrategyApi.OTHER_PACKAGES, + generationTimeoutInMillis: Long = UtSettings.utBotGenerationTimeoutInMillis, + primitiveValuesSupplier: CustomFuzzerValueSupplier = CustomFuzzerValueSupplier { null } + ): MutableList { + fun createPrimitiveModels(supplier: CustomFuzzerValueSupplier, classId: ClassId): Sequence = + supplier + .takeIf { classId.isPrimitive || classId.isPrimitiveWrapper || classId == stringClassId } + ?.get(classId.jClass) + ?.asSequence() + ?.filter { + val valueClassId = it.javaClass.id + when { + classId == valueClassId -> true + classId.isPrimitive -> wrapperByPrimitive[classId] == valueClassId + classId.isPrimitiveWrapper -> primitiveByWrapper[classId] == valueClassId + else -> false + } + } + ?.map { UtPrimitiveModel(it) } ?: emptySequence() + + val customModelProvider = ModelProvider { description, consumer -> + description.parametersMap.forEach { (classId, indices) -> + createPrimitiveModels(primitiveValuesSupplier, classId).forEach { model -> + indices.forEach { index -> + consumer.accept(index, FuzzedValue(model)) + } + } + } + } + + return withUtContext(UtContext(classUnderTest.classLoader)) { + UtBotTestCaseGenerator + .apply { + init( + FileUtil.isolateClassFiles(classUnderTest.kotlin).toPath(), classpath, dependencyClassPath + ) + }.generateForSeveralMethods( + methodsForAutomaticGeneration.map { + toUtMethod( + it.methodToBeTestedFromUserInput, + classUnderTest.kotlin + ) + }, + mockStrategyApi, + chosenClassesToMockAlways = emptySet(), + generationTimeoutInMillis, + generate = { symbolicEngine -> + symbolicEngine.fuzzing { defaultModelProvider -> + customModelProvider.withFallback(defaultModelProvider) + } + } + ) + }.toMutableList() + } + private fun generateUnitTests( concreteExecutor: ConcreteExecutor, testMethods: List, @@ -182,4 +265,19 @@ object UtBotJavaApi { listOf(utExecution) ) }.toList() +} + +/** + * Accepts type of parameter and returns collection of values for this type. + * + * Value types which can be generated: + * + * - primitive types: boolean, char, byte, short, int, long, float, double + * - primitive wrappers: Boolean, Character, Byte, Short, Integer, Long, Float, Double + * - String + * + * If null is returned instead of empty collection then default fuzzer values are produced. + */ +fun interface CustomFuzzerValueSupplier { + fun get(type: Class<*>): Collection? } \ No newline at end of file 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 fad4158d4d..1842f652c6 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 @@ -38,6 +38,7 @@ import org.utbot.framework.plugin.api.util.defaultValueModel import org.utbot.framework.plugin.api.util.executableId import org.utbot.framework.plugin.api.util.jClass import org.utbot.framework.util.nextModelName +import java.lang.reflect.Constructor import java.util.IdentityHashMap /** @@ -195,19 +196,24 @@ class AssembleModelGenerator(private val methodUnderTest: UtMethod<*>) { /** * Assembles internal structure of [UtArrayModel]. */ - private fun assembleArrayModel(arrayModel: UtArrayModel): UtModel { - instantiatedModels[arrayModel]?.let { return it } + private fun assembleArrayModel(arrayModel: UtArrayModel): UtModel = + with(arrayModel) { + instantiatedModels[this]?.let { return it } - val constModel = assembleModel(arrayModel.constModel) - val stores = arrayModel.stores - .mapValues { assembleModel(it.value) } - .toMutableMap() + // Note that we use constModel from the source model as is here to avoid + // possible stack overflow error in case when const model has the same + // id as the source one. Later we will try to transform it. + val assembleModel = UtArrayModel(id, classId, length, constModel, stores = mutableMapOf()) - val assembleModel = UtArrayModel(arrayModel.id, arrayModel.classId, arrayModel.length, constModel, stores) + instantiatedModels[this] = assembleModel - instantiatedModels[arrayModel] = assembleModel - return assembleModel - } + assembleModel.constModel = assembleModel(constModel) + assembleModel.stores += stores + .mapValues { assembleModel(it.value) } + .toMutableMap() + + assembleModel + } /** * Assembles internal structure of [UtCompositeModel] if possible and handles assembling exceptions. @@ -251,7 +257,7 @@ class AssembleModelGenerator(private val methodUnderTest: UtMethod<*>) { } //fill field value if it hasn't been filled by constructor, and it is not default if (fieldId in constructorInfo.affectedFields || - (fieldId !in constructorInfo.setFields && !fieldModel.hasDefaultValue()) + (fieldId !in constructorInfo.setFields && !fieldModel.hasDefaultValue()) ) { val modifierCall = modifierCall(this, fieldId, assembleModel(fieldModel)) callChain.add(modifierCall) @@ -352,15 +358,21 @@ class AssembleModelGenerator(private val methodUnderTest: UtMethod<*>) { */ private fun findBestConstructorOrNull(compositeModel: UtCompositeModel): ConstructorId? { val classId = compositeModel.classId - if (!classId.isPublic || classId.isInner) return null + if (!classId.isVisible || classId.isInner) return null return classId.jClass.declaredConstructors - .filter { it.isPublic || !it.isPrivate && it.declaringClass.packageName.startsWith(methodPackageName) } + .filter { it.isVisible } .sortedByDescending { it.parameterCount } .map { it.executableId } .firstOrNull { constructorAnalyzer.isAppropriate(it) } } + private val ClassId.isVisible : Boolean + get() = this.isPublic || !this.isPrivate && this.packageName.startsWith(methodPackageName) + + private val Constructor<*>.isVisible : Boolean + get() = this.isPublic || !this.isPrivate && this.declaringClass.packageName.startsWith(methodPackageName) + /** * Creates setter or direct setter call to set a field. * 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 40e751b6c0..c96347f4c7 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 @@ -115,7 +115,7 @@ sealed class StaticsMocking( object NoStaticMocking : StaticsMocking( displayName = "No static mocking", - description = "Don't use additional settings to mock static fields" + description = "Do not use additional settings to mock static fields" ) object MockitoStaticMocking : StaticsMocking(displayName = "Mockito static mocking") { @@ -505,7 +505,7 @@ data class HangingTestsTimeout(val timeoutMs: Long) { companion object { const val DEFAULT_TIMEOUT_MS = DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS - const val MIN_TIMEOUT_MS = DEFAULT_CONCRETE_EXECUTION_TIMEOUT_IN_CHILD_PROCESS_MS + const val MIN_TIMEOUT_MS = 100L const val MAX_TIMEOUT_MS = 1_000_000L } } @@ -551,7 +551,7 @@ enum class ParametrizedTestSource( ) : CodeGenerationSettingItem { DO_NOT_PARAMETRIZE( displayName = "Not parametrized", - description = "Don't generate parametrized tests" + description = "Do not generate parametrized tests" ), PARAMETRIZE( displayName = "Parametrized", diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/MockitoBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/MockitoBuiltins.kt index 5073b37aaa..1a2805b615 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/MockitoBuiltins.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/MockitoBuiltins.kt @@ -1,6 +1,7 @@ package org.utbot.framework.codegen.model.constructor.builtin import org.utbot.framework.plugin.api.BuiltinClassId +import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.builtinMethodId import org.utbot.framework.plugin.api.util.builtinStaticMethodId @@ -15,6 +16,13 @@ import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.plugin.api.util.shortClassId import org.utbot.framework.plugin.api.util.stringClassId +internal val mockitoBuiltins: Set + get() = setOf( + mockMethodId, whenMethodId, thenMethodId, thenReturnMethodId, + any, anyOfClass, anyByte, anyChar, anyShort, anyInt, anyLong, + anyFloat, anyDouble, anyBoolean, anyString + ) + internal val mockitoClassId = BuiltinClassId( name = "org.mockito.Mockito", canonicalName = "org.mockito.Mockito", diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/ReflectionBuiltins.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/ReflectionBuiltins.kt index 9516e9e186..dd5acb019f 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/ReflectionBuiltins.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/constructor/builtin/ReflectionBuiltins.kt @@ -1,6 +1,7 @@ package org.utbot.framework.codegen.model.constructor.builtin import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.intClassId @@ -8,13 +9,25 @@ import org.utbot.framework.plugin.api.util.methodId import org.utbot.framework.plugin.api.util.objectClassId import org.utbot.framework.plugin.api.util.stringClassId import org.utbot.framework.plugin.api.util.voidClassId +import sun.misc.Unsafe import java.lang.reflect.AccessibleObject import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException -import sun.misc.Unsafe // reflection methods ids +//TODO: these methods are called builtins, but actually are just [MethodId] +//may be fixed in https://github.com/UnitTestBot/UTBotJava/issues/138 + +internal val reflectionBuiltins: Set + get() = setOf( + setAccessible, invoke, newInstance, get, forName, + getDeclaredMethod, getDeclaredConstructor, allocateInstance, + getClass, getDeclaredField, isEnumConstant, getFieldName, + equals, getSuperclass, set, newArrayInstance, + setArrayElement, getArrayElement, getTargetException, + ) + internal val setAccessible = methodId( classId = AccessibleObject::class.id, name = "setAccessible", 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 28210145bf..9169e42c44 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 @@ -33,6 +33,7 @@ internal val ClassId.possibleUtilMethodIds: Set deepEqualsMethodId, arraysDeepEqualsMethodId, iterablesDeepEqualsMethodId, + streamsDeepEqualsMethodId, mapsDeepEqualsMethodId, hasCustomEqualsMethodId, getArrayLengthMethodId @@ -117,6 +118,13 @@ internal val ClassId.iterablesDeepEqualsMethodId: MethodId arguments = arrayOf(java.lang.Iterable::class.id, java.lang.Iterable::class.id) ) +internal val ClassId.streamsDeepEqualsMethodId: MethodId + get() = utilMethodId( + name = "streamsDeepEquals", + returnType = booleanClassId, + arguments = arrayOf(java.util.stream.Stream::class.id, java.util.stream.Stream::class.id) + ) + internal val ClassId.mapsDeepEqualsMethodId: MethodId get() = utilMethodId( name = "mapsDeepEquals", 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 e2ae9ccafb..a7c3dc4216 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 @@ -54,6 +54,7 @@ import kotlinx.collections.immutable.PersistentSet import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.persistentSetOf +import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId /** * Interface for all code generation context aware entities @@ -351,6 +352,9 @@ internal interface CgContextOwner { val iterablesDeepEquals: MethodId get() = currentTestClass.iterablesDeepEqualsMethodId + val streamsDeepEquals: MethodId + get() = currentTestClass.streamsDeepEqualsMethodId + val mapsDeepEquals: MethodId get() = currentTestClass.mapsDeepEqualsMethodId 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 208b8b3459..19acd0ba98 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 @@ -1,13 +1,32 @@ package org.utbot.framework.codegen.model.constructor.tree +import kotlinx.collections.immutable.PersistentList +import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.builtin.any import org.utbot.framework.codegen.model.constructor.builtin.anyOfClass +import org.utbot.framework.codegen.model.constructor.builtin.arraysDeepEqualsMethodId +import org.utbot.framework.codegen.model.constructor.builtin.createArrayMethodId +import org.utbot.framework.codegen.model.constructor.builtin.createInstanceMethodId +import org.utbot.framework.codegen.model.constructor.builtin.deepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.forName +import org.utbot.framework.codegen.model.constructor.builtin.getArrayLengthMethodId import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredConstructor import org.utbot.framework.codegen.model.constructor.builtin.getDeclaredMethod +import org.utbot.framework.codegen.model.constructor.builtin.getEnumConstantByNameMethodId +import org.utbot.framework.codegen.model.constructor.builtin.getFieldValueMethodId +import org.utbot.framework.codegen.model.constructor.builtin.getStaticFieldValueMethodId +import org.utbot.framework.codegen.model.constructor.builtin.getTargetException +import org.utbot.framework.codegen.model.constructor.builtin.getUnsafeInstanceMethodId +import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.invoke +import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId +import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.newInstance import org.utbot.framework.codegen.model.constructor.builtin.setAccessible +import org.utbot.framework.codegen.model.constructor.builtin.setFieldMethodId +import org.utbot.framework.codegen.model.constructor.builtin.setStaticFieldMethodId +import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId 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.util.CgComponents @@ -31,18 +50,20 @@ import org.utbot.framework.codegen.model.util.at import org.utbot.framework.codegen.model.util.isAccessibleFrom import org.utbot.framework.codegen.model.util.nullLiteral import org.utbot.framework.codegen.model.util.resolve +import org.utbot.framework.plugin.api.BuiltinMethodId import org.utbot.framework.plugin.api.ClassId 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.UtExplicitlyThrownException import org.utbot.framework.plugin.api.util.exceptions import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.isArray import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.method import org.utbot.framework.plugin.api.util.objectArrayClassId import org.utbot.framework.plugin.api.util.objectClassId -import kotlinx.collections.immutable.PersistentList typealias Block = PersistentList @@ -99,25 +120,69 @@ internal class CgCallableAccessManagerImpl(val context: CgContext) : CgCallableA return methodCall } - // TODO: do not always use Throwable in the future, use more specific exceptions instead - private fun newMethodCall(id: MethodId) { - when { - id.isUtil -> { - requiredUtilMethods += id - addException(java.lang.Throwable::class.id) - } - else -> addException(java.lang.Throwable::class.id) + private fun newMethodCall(methodId: MethodId) { + if (methodId.isUtil) requiredUtilMethods += methodId + importIfNeeded(methodId) + + //Builtin methods does not have jClass, so [methodId.method] will crash on it, + //so we need to collect required exceptions manually from source codes + if (methodId is BuiltinMethodId) { + methodId.findExceptionTypes().forEach { addException(it) } + return + } + //If [InvocationTargetException] is thrown manually in test, we need + // to add "throws Throwable" and other exceptions are not required so on. + if (methodId == getTargetException) { + collectedExceptions.clear() + addException(Throwable::class.id) + return + } + + val methodIsUnderTestAndThrowsExplicitly = methodId == currentExecutable + && currentExecution?.result is UtExplicitlyThrownException + val frameworkSupportsAssertThrows = testFramework == Junit5 || testFramework == TestNg + + //If explicit exception is wrapped with assertThrows, + // no "throws" in test method signature is required. + if (methodIsUnderTestAndThrowsExplicitly && frameworkSupportsAssertThrows) { + return } - importIfNeeded(id) + + methodId.method.exceptionTypes.forEach { addException(it.id) } } - private fun newConstructorCall(id: ConstructorId) { - importIfNeeded(id.classId) - for (exception in id.exceptions) { + private fun newConstructorCall(constructorId: ConstructorId) { + importIfNeeded(constructorId.classId) + for (exception in constructorId.exceptions) { addException(exception) } } + private fun BuiltinMethodId.findExceptionTypes(): Set { + if (!this.isUtil) return emptySet() + + with(currentTestClass) { + return when (this@findExceptionTypes) { + getEnumConstantByNameMethodId -> setOf(IllegalAccessException::class.id) + getStaticFieldValueMethodId, + getFieldValueMethodId, + setStaticFieldMethodId, + setFieldMethodId, + createInstanceMethodId, + getUnsafeInstanceMethodId -> setOf(Exception::class.id) + createArrayMethodId -> setOf(ClassNotFoundException::class.id) + deepEqualsMethodId, + arraysDeepEqualsMethodId, + iterablesDeepEqualsMethodId, + streamsDeepEqualsMethodId, + mapsDeepEqualsMethodId, + hasCustomEqualsMethodId, + getArrayLengthMethodId -> emptySet() + else -> error("Unknown util method $this") + } + } + } + private infix fun CgExpression?.canBeReceiverOf(executable: MethodId): Boolean = when { // TODO: rewrite by using CgMethodId, etc. 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 adc28f3ab2..57ea4d2ffb 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 @@ -7,6 +7,7 @@ import org.utbot.framework.codegen.ForceStaticMocking import org.utbot.framework.codegen.JUNIT5_PARAMETERIZED_PACKAGE import org.utbot.framework.codegen.Junit4 import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.ParametrizedTestSource import org.utbot.framework.codegen.RuntimeExceptionTestsBehaviour.PASS import org.utbot.framework.codegen.TestNg import org.utbot.framework.codegen.model.constructor.builtin.closeMethodIdOrNull @@ -107,6 +108,7 @@ import org.utbot.framework.plugin.api.UtEnumConstantModel import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtExecutionFailure import org.utbot.framework.plugin.api.UtExecutionSuccess +import org.utbot.framework.plugin.api.UtExplicitlyThrownException import org.utbot.framework.plugin.api.UtMethod import org.utbot.framework.plugin.api.UtModel import org.utbot.framework.plugin.api.UtNewInstanceInstrumentation @@ -292,7 +294,8 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c // we cannot generate any assertions for constructor testing // but we need to generate a constructor call val constructorCall = currentExecutable as ConstructorId - currentExecution!!.result + val currentExecution = currentExecution!! + currentExecution.result .onSuccess { methodType = SUCCESSFUL @@ -308,15 +311,16 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } .onFailure { exception -> - processExecutionFailure(exception) + processExecutionFailure(currentExecution, exception) } } is BuiltinMethodId -> error("Unexpected BuiltinMethodId $currentExecutable while generating result assertions") is MethodId -> { emptyLineIfNeeded() val method = currentExecutable as MethodId + val currentExecution = currentExecution!! // build assertions - currentExecution!!.result + currentExecution.result .onSuccess { result -> methodType = SUCCESSFUL @@ -330,13 +334,13 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } .onFailure { exception -> - processExecutionFailure(exception) + processExecutionFailure(currentExecution, exception) } } } } - private fun processExecutionFailure(exception: Throwable) { + private fun processExecutionFailure(execution: UtExecution, exception: Throwable) { val methodInvocationBlock = { with(currentExecutable) { when (this) { @@ -346,7 +350,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } } - if (shouldTestPassWithExpectedException(exception)) { + if (shouldTestPassWithException(execution, exception)) { testFrameworkManager.expectException(exception::class.id) { methodInvocationBlock() } @@ -373,11 +377,13 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c methodInvocationBlock() } - private fun shouldTestPassWithExpectedException(exception: Throwable): Boolean { + private fun shouldTestPassWithException(execution: UtExecution, exception: Throwable): Boolean { // tests with timeout or crash should be processed differently if (exception is TimeoutException || exception is ConcreteExecutionFailureException) return false - return runtimeExceptionTestsBehaviour == PASS || exception !is RuntimeException + val exceptionRequiresAssert = exception !is RuntimeException || runtimeExceptionTestsBehaviour == PASS + val exceptionIsExplicit = execution.result is UtExplicitlyThrownException + return exceptionRequiresAssert || exceptionIsExplicit } private fun writeWarningAboutTimeoutExceeding() { @@ -528,10 +534,15 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c doubleDelta ) expectedModel.value is Boolean -> { - if (expectedModel.value as Boolean) { - assertions[assertTrue](actual) - } else { - assertions[assertFalse](actual) + when (parameterizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> + if (expectedModel.value as Boolean) { + assertions[assertTrue](actual) + } else { + assertions[assertFalse](actual) + } + ParametrizedTestSource.PARAMETRIZE -> + assertions[assertEquals](expected, actual) } } // other primitives and string @@ -843,8 +854,14 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c depth: Int, visitedModels: MutableSet ) { + // if field is static, it is represents itself in "before" and + // "after" state: no need to assert its equality to itself. + if (fieldId.isStatic) { + return + } + + // if model is already processed, so we don't want to add new statements if (fieldModel in visitedModels) { - // this model is already processed, so we don't want to add new statements return } @@ -985,7 +1002,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c "but `${resultModel::class}` found" } - generateDeepEqualsAssertion(expected, actual) + generateDeepEqualsOrNullAssertion(expected, actual) } } } @@ -1006,10 +1023,14 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c ) } expected == nullLiteral() -> testFrameworkManager.assertNull(actual) - expected is CgLiteral && expected.value is Boolean -> testFrameworkManager.assertBoolean( - expected.value, - actual - ) + expected is CgLiteral && expected.value is Boolean -> { + when (parameterizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> + testFrameworkManager.assertBoolean(expected.value, actual) + ParametrizedTestSource.PARAMETRIZE -> + testFrameworkManager.assertEquals(expected, actual) + } + } else -> { if (expected is CgLiteral) { // Literal can only be Primitive or String, can use equals here @@ -1017,16 +1038,41 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c return } - generateDeepEqualsAssertion(expected, actual) + generateDeepEqualsOrNullAssertion(expected, actual) } } } } - private fun generateDeepEqualsAssertion( + /** + * We can't use standard deepEquals method in parametrized tests + * because nullable objects require different asserts. + * See https://github.com/UnitTestBot/UTBotJava/issues/252 for more details. + */ + private fun generateDeepEqualsOrNullAssertion( expected: CgValue, - actual: CgVariable + actual: CgVariable, ) { + when (parameterizedTestSource) { + ParametrizedTestSource.DO_NOT_PARAMETRIZE -> + currentBlock = currentBlock.addAll(generateDeepEqualsAssertion(expected, actual)) + ParametrizedTestSource.PARAMETRIZE -> { + val assertNullStmt = listOf(testFrameworkManager.assertions[testFramework.assertNull](actual).toStatement()) + currentBlock = currentBlock.add( + CgIfStatement( + CgEqualTo(expected, nullLiteral()), + assertNullStmt, + generateDeepEqualsAssertion(expected, actual) + ) + ) + } + } + } + + private fun generateDeepEqualsAssertion( + expected: CgValue, + actual: CgVariable, + ): List { require(expected is CgVariable) { "Expected value have to be Literal or Variable but `${expected::class}` found" } @@ -1040,7 +1086,8 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c depth = 0, visitedModels = hashSetOf() ) - currentBlock = currentBlock.addAll(statements.dropLastWhile { it is CgEmptyLine }) + + return statements.dropLastWhile { it is CgEmptyLine } } private fun recordActualResult() { @@ -1129,7 +1176,7 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c val varType = CgClassId( it.variableType, TypeParameters(listOf(typeParameter)), - isNullable=true, + isNullable = true, ) +CgDeclaration( varType, @@ -1155,7 +1202,9 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c } //TODO: orientation on arbitrary execution may be misleading, but what is the alternative? - val arbitraryExecution = utTestCase.executions.firstOrNull { it.result is UtExecutionSuccess } + //may be a heuristic to select a model with minimal number of internal nulls should be used + val arbitraryExecution = utTestCase.executions + .firstOrNull { it.result is UtExecutionSuccess && (it.result as UtExecutionSuccess).model !is UtNullModel } ?: utTestCase.executions.first() return withTestMethodScope(arbitraryExecution) { @@ -1206,7 +1255,6 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c //record result and generate result assertions recordActualResult() generateAssertionsForParameterizedTest() - } methodType = PARAMETRIZED @@ -1633,21 +1681,14 @@ internal class CgMethodConstructor(val context: CgContext) : CgContextOwner by c * that may be thrown by these calls. */ private fun CgExecutableCall.intercepted() { - when (executableId) { - is MethodId -> { - if (executableId == invoke) { - this.wrapReflectiveCall() - } else { - +this - } - } - is ConstructorId -> { - if (executableId == newInstance) { - this.wrapReflectiveCall() - } else { - +this - } - } + val executableToWrap = when (executableId) { + is MethodId -> invoke + is ConstructorId -> newInstance + } + if (executableId == executableToWrap) { + this.wrapReflectiveCall() + } else { + +this } } } \ No newline at end of file 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 ed2ab2d6bc..83c77d7d6e 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 @@ -162,8 +162,8 @@ internal class CgTestClassConstructor(val context: CgContext) : */ private fun MethodId.dependencies(): List = when (this) { createInstance -> listOf(getUnsafeInstance) - deepEquals -> listOf(arraysDeepEquals, iterablesDeepEquals, mapsDeepEquals, hasCustomEquals) - arraysDeepEquals, iterablesDeepEquals, mapsDeepEquals -> listOf(deepEquals) + deepEquals -> listOf(arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals, hasCustomEquals) + arraysDeepEquals, iterablesDeepEquals, streamsDeepEquals, mapsDeepEquals -> listOf(deepEquals) else -> emptyList() } @@ -186,7 +186,12 @@ data class TestsGenerationReport( var errors: MutableMap, ErrorsCount> = mutableMapOf() ) { val classUnderTest: KClass<*> - get() = executables.firstOrNull()?.clazz ?: error("No executables found in test report") + get() = executables.firstOrNull()?.clazz + ?: error("No executables found in test report") + + // Summary message is generated lazily to avoid evaluation of classUnderTest + var summaryMessage: () -> String = { "Unit tests for $classUnderTest were generated successfully." } + val initialWarnings: MutableList<() -> String> = mutableListOf() fun addMethodErrors(testCase: UtTestCase, errors: Map) { this.errors[testCase.method] = errors @@ -212,7 +217,11 @@ data class TestsGenerationReport( } override fun toString(): String = buildString { - appendHtmlLine("Unit tests for $classUnderTest were generated successfully.") + appendHtmlLine(summaryMessage()) + appendHtmlLine() + initialWarnings.forEach { appendHtmlLine(it()) } + appendHtmlLine() + val testMethodsStatistic = executables.map { it.countTestMethods() } val errors = executables.map { it.countErrors() } val overallTestMethods = testMethodsStatistic.sumBy { it.count } 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 43d4092409..534505dec5 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 @@ -7,9 +7,11 @@ import org.utbot.framework.codegen.model.constructor.context.CgContextOwner 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.get +import org.utbot.framework.codegen.model.constructor.util.isDefaultValueOf import org.utbot.framework.codegen.model.constructor.util.isNotDefaultValueOf import org.utbot.framework.codegen.model.constructor.util.typeCast import org.utbot.framework.codegen.model.tree.CgAllocateArray +import org.utbot.framework.codegen.model.tree.CgAllocateInitializedArray import org.utbot.framework.codegen.model.tree.CgDeclaration import org.utbot.framework.codegen.model.tree.CgEnumConstantAccess import org.utbot.framework.codegen.model.tree.CgExpression @@ -222,49 +224,63 @@ internal class CgVariableConstructor(val context: CgContext) : return CgLiteral(classId, paramModel.value) } - private fun constructArray(model: UtArrayModel, baseName: String?): CgVariable { - val elementType = model.classId.elementClassId!! - val array = newVar(model.classId, baseName) { CgAllocateArray(model.classId, elementType, model.length) } - valueByModelId[model.id] = array - if (model.length <= 0) return array - if (model.length == 1) { + private fun constructArray(arrayModel: UtArrayModel, baseName: String?): CgVariable { + val elementType = arrayModel.classId.elementClassId!! + val elementModels = (0 until arrayModel.length).map { + arrayModel.stores.getOrDefault(it, arrayModel.constModel) + } + + val canInitWithValues = elementModels.all { it is UtPrimitiveModel } || elementModels.all { it is UtNullModel } + + val initializer = if (canInitWithValues) { + CgAllocateInitializedArray(arrayModel) + } else { + CgAllocateArray(arrayModel.classId, elementType, arrayModel.length) + } + + val array = newVar(arrayModel.classId, baseName) { initializer } + valueByModelId[arrayModel.id] = array + + if (canInitWithValues) { + return array + } + + if (arrayModel.length <= 0) return array + if (arrayModel.length == 1) { // take first element value if it is present, otherwise use default value from model - val elementModel = model[0] + val elementModel = arrayModel[0] if (elementModel isNotDefaultValueOf elementType) { array.setArrayElement(0, getOrCreateVariable(elementModel)) } } else { - val indexedValuesFromStores = if (model.stores.size == model.length) { - // do not use constModel because stores fully cover array - - // choose all not default values from stores - model.stores.entries.filter { (_, element) -> element isNotDefaultValueOf elementType } - } else { - val initialValue = when { - model.constModel isNotDefaultValueOf elementType -> model.constModel - else -> elementType.defaultValueModel() - } + val indexedValuesFromStores = + if (arrayModel.stores.size == arrayModel.length) { + // do not use constModel because stores fully cover array + arrayModel.stores.entries.filter { (_, element) -> element isNotDefaultValueOf elementType } + } else { + // fill array if constModel is not default type value + if (arrayModel.constModel isNotDefaultValueOf elementType) { + val defaultVariable = getOrCreateVariable(arrayModel.constModel, "defaultValue") + basicForLoop(arrayModel.length) { i -> + array.setArrayElement(i, defaultVariable) + } + } - // fill array via constModel if it is not default type value - if (model.constModel isNotDefaultValueOf elementType) { - // fill array with default values from model - val defaultValue = getOrCreateVariable(model.constModel, "defaultValue") - basicForLoop(model.length) { i -> - array.setArrayElement(i, defaultValue) + // choose all not default values + val defaultValue = if (arrayModel.constModel isDefaultValueOf elementType) { + arrayModel.constModel + } else { + elementType.defaultValueModel() } + arrayModel.stores.entries.filter { (_, element) -> element != defaultValue } } - // choose all not default values - model.stores.entries.filter { (_, element) -> element != initialValue } - } - - val sortedByIndexValuesFromStores = indexedValuesFromStores.sortedBy { it.key } - // set all values from stores manually - sortedByIndexValuesFromStores.forEach { (index, element) -> - array.setArrayElement(index, getOrCreateVariable(element)) - } + indexedValuesFromStores + .sortedBy { it.key } + .forEach { (index, element) -> array.setArrayElement(index, getOrCreateVariable(element)) } } + return array } @@ -299,7 +315,10 @@ internal class CgVariableConstructor(val context: CgContext) : /** * [indexedValuesFromStores] have to be continuous sorted range */ - private fun setStoresRange(array: CgVariable, indexedValuesFromStores: List>) { + private fun setStoresRange( + array: CgVariable, + indexedValuesFromStores: List> + ) { if (indexedValuesFromStores.size < 3) { // range is too small, better set manually indexedValuesFromStores.forEach { (index, element) -> 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 6b3c570520..7b8ed610b9 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 @@ -9,6 +9,7 @@ import org.utbot.framework.codegen.model.constructor.builtin.forName import org.utbot.framework.codegen.model.constructor.builtin.hasCustomEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId +import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId 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.util.CgComponents @@ -103,6 +104,7 @@ internal abstract class TestFrameworkManager(val context: CgContext) requiredUtilMethods += currentTestClass.deepEqualsMethodId requiredUtilMethods += currentTestClass.arraysDeepEqualsMethodId requiredUtilMethods += currentTestClass.iterablesDeepEqualsMethodId + requiredUtilMethods += currentTestClass.streamsDeepEqualsMethodId requiredUtilMethods += currentTestClass.mapsDeepEqualsMethodId requiredUtilMethods += currentTestClass.hasCustomEqualsMethodId @@ -147,9 +149,10 @@ internal abstract class TestFrameworkManager(val context: CgContext) fun assertBoolean(actual: CgExpression) = assertBoolean(expected = true, actual) - // Exception expectation is very different in JUnit4 and JUnit5 + // Exception expectation differs between test frameworks // JUnit4 requires to add a specific argument to the test method annotation - // JUnit5 requires to use method assertThrows() + // JUnit5 requires using method assertThrows() + // TestNg allows both approaches, we use similar to JUnit5 abstract fun expectException(exception: ClassId, block: () -> Unit) open fun setTestExecutionTimeout(timeoutMs: Long) { 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 535451a242..c6fa56305c 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 @@ -169,6 +169,8 @@ internal fun CgContextOwner.importIfNeeded(type: ClassId) { .takeIf { (it.isRefType && it.packageName != testClassPackageName && it.packageName != "java.lang") || it.isNested } // we cannot import inaccessible classes (builtin classes like JUnit are accessible here because they are public) ?.takeIf { it.isAccessibleFrom(testClassPackageName) } + // don't import classes from default package + ?.takeIf { !it.isInDefaultPackage } // cannot import anonymous classes ?.takeIf { !it.isAnonymous } // do not import if there is a simple name clash 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 121a6ee760..c3630a7192 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 @@ -18,6 +18,7 @@ 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.TypeParameters +import org.utbot.framework.plugin.api.UtArrayModel import org.utbot.framework.plugin.api.util.booleanClassId import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.intClassId @@ -81,6 +82,7 @@ interface CgElement { is CgLiteral -> visit(element) is CgNonStaticRunnable -> visit(element) is CgStaticRunnable -> visit(element) + is CgAllocateInitializedArray -> visit(element) is CgAllocateArray -> visit(element) is CgEnumConstantAccess -> visit(element) is CgFieldAccess -> visit(element) @@ -644,7 +646,7 @@ class CgStaticRunnable(type: ClassId, val classId: ClassId, methodId: MethodId): // Array allocation -class CgAllocateArray(type: ClassId, elementType: ClassId, val size: Int) +open class CgAllocateArray(type: ClassId, elementType: ClassId, val size: Int) : CgReferenceExpression { override val type: ClassId by lazy { CgClassId(type.name, updateElementType(elementType), isNullable = type.isNullable) } val elementType: ClassId by lazy { @@ -662,6 +664,10 @@ class CgAllocateArray(type: ClassId, elementType: ClassId, val size: Int) } } +class CgAllocateInitializedArray(val model: UtArrayModel) + : CgAllocateArray(model.classId, model.classId.elementClassId!!, model.length) + + // Spread operator (for Kotlin, empty for Java) class CgSpread(override val type: ClassId, val array: CgExpression): CgExpression 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 8a36e18fa7..5edbf60ec6 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 @@ -22,6 +22,7 @@ import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.FieldId import org.utbot.framework.plugin.api.MethodId import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId import org.utbot.framework.plugin.api.util.charClassId import org.utbot.framework.plugin.api.util.doubleClassId import org.utbot.framework.plugin.api.util.floatClassId @@ -53,7 +54,7 @@ fun intLiteral(num: Int) = CgLiteral(intClassId, num) fun longLiteral(num: Long) = CgLiteral(longClassId, num) -fun byteLiteral(num: Byte) = CgLiteral(booleanClassId, num) +fun byteLiteral(num: Byte) = CgLiteral(byteClassId, num) fun shortLiteral(num: Short) = CgLiteral(shortClassId, num) 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 e9c06b873e..866787fb17 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 @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.model.visitor +import org.apache.commons.text.StringEscapeUtils import org.utbot.common.WorkaroundReason.LONG_CODE_FRAGMENTS import org.utbot.common.workaround import org.utbot.framework.codegen.Import @@ -77,11 +78,23 @@ import org.utbot.framework.codegen.model.tree.CgVariable import org.utbot.framework.codegen.model.tree.CgWhileLoop import org.utbot.framework.codegen.model.util.CgPrinter import org.utbot.framework.codegen.model.util.CgPrinterImpl +import org.utbot.framework.codegen.model.util.resolve 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.TypeParameters -import org.apache.commons.text.StringEscapeUtils +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.charClassId +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 internal abstract class CgAbstractRenderer(val context: CgContext, val printer: CgPrinter = CgPrinterImpl()) : CgVisitor, CgPrinter by printer { @@ -98,6 +111,17 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: protected abstract val langPackage: String + //We may render array elements in initializer in one line or in separate lines: + //items count in one line depends on the value type. + protected fun arrayElementsInLine(constModel: UtModel): Int { + if (constModel is UtNullModel) return 10 + return when (constModel.classId) { + intClassId, byteClassId, longClassId, charClassId -> 8 + booleanClassId, shortClassId, doubleClassId, floatClassId -> 6 + else -> error("Non primitive value of type ${constModel.classId} is unexpected in array initializer") + } + } + private val MethodId.accessibleByName: Boolean get() = (context.shouldOptimizeImports && this in context.importedStaticMethods) || classId == context.currentTestClass @@ -689,6 +713,17 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: protected abstract fun renderExceptionCatchVariable(exception: CgVariable) + protected fun UtArrayModel.getElementExpr(index: Int): CgExpression { + val itemModel = stores.getOrDefault(index, constModel) + val cgValue: CgExpression = when (itemModel) { + is UtPrimitiveModel -> itemModel.value.resolve() + is UtNullModel -> null.resolve() + else -> error("Non primitive or null model $itemModel is unexpected in array initializer") + } + + return cgValue + } + protected fun getEscapedImportRendering(import: Import): String = import.qualifiedName .split(".") @@ -720,6 +755,34 @@ internal abstract class CgAbstractRenderer(val context: CgContext, val printer: } } + protected fun UtArrayModel.renderElements(length: Int, elementsInLine: Int) { + if (length <= elementsInLine) { // one-line array + for (i in 0 until length) { + val expr = this.getElementExpr(i) + expr.accept(this@CgAbstractRenderer) + if (i != length - 1) { + print(", ") + } + } + } else { // multiline array + println() // line break after `int[] x = {` + withIndent { + for (i in 0 until length) { + val expr = this.getElementExpr(i) + expr.accept(this@CgAbstractRenderer) + + if (i == length - 1) { + println() + } else if (i % elementsInLine == elementsInLine - 1) { + println(",") + } else { + print(", ") + } + } + } + } + } + protected inline fun withIndent(block: () -> Unit) { try { pushIndent() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt index ddcb55db47..751057a1ab 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/codegen/model/visitor/CgJavaRenderer.kt @@ -1,9 +1,11 @@ package org.utbot.framework.codegen.model.visitor +import org.apache.commons.text.StringEscapeUtils import org.utbot.framework.codegen.RegularImport import org.utbot.framework.codegen.StaticImport 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.CgBreakStatement @@ -39,9 +41,9 @@ 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.wrapperByPrimitive -import org.apache.commons.text.StringEscapeUtils -internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { +internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : + CgAbstractRenderer(context, printer) { override val statementEnding: String = ";" @@ -103,20 +105,19 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter } override fun visit(element: CgTypeCast) { - // TODO: check cases when element.expression is CgLiteral of primitive wrapper type and element.targetType is primitive - // TODO: example: (double) 1.0, (float) 1.0f, etc. + val expr = element.expression + val wrappedTargetType = wrapperByPrimitive.getOrDefault(element.targetType, element.targetType) + val exprTypeIsSimilar = expr.type == element.targetType || expr.type == wrappedTargetType + // cast for null is mandatory in case of ambiguity - for example, readObject(Object) and readObject(Map) - if (element.expression.type == element.targetType && element.expression != nullLiteral()) { + if (exprTypeIsSimilar && expr != nullLiteral()) { element.expression.accept(this) return } - val elementTargetType = element.targetType - val targetType = wrapperByPrimitive.getOrDefault(elementTargetType, elementTargetType) - print("(") print("(") - print(targetType.asString()) + print(wrappedTargetType.asString()) print(") ") element.expression.accept(this) print(")") @@ -154,11 +155,19 @@ internal class CgJavaRenderer(context: CgContext, printer: CgPrinter = CgPrinter } override fun visit(element: CgAllocateArray) { - // TODO: definitely rewrite later - // TODO: check array type rendering - val beforeLength = element.type.canonicalName.substringBefore("[") - val afterLength = element.type.canonicalName.substringAfter("]") - print("new $beforeLength[${element.size}]$afterLength") + // TODO: Arsen strongly required to rewrite later + val typeName = element.type.canonicalName.substringBefore("[") + val otherDimensions = element.type.canonicalName.substringAfter("]") + print("new $typeName[${element.size}]$otherDimensions") + } + + override fun visit(element: CgAllocateInitializedArray) { + val arrayModel = element.model + val elementsInLine = arrayElementsInLine(arrayModel.constModel) + + print("{") + arrayModel.renderElements(element.size, elementsInLine) + print("}") } override fun visit(element: CgGetLength) { 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 4817372fcf..c5fe8c3bd0 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 @@ -1,5 +1,6 @@ package org.utbot.framework.codegen.model.visitor +import org.apache.commons.text.StringEscapeUtils import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.framework.codegen.RegularImport @@ -7,6 +8,7 @@ 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 @@ -43,6 +45,7 @@ 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.TypeParameters +import org.utbot.framework.plugin.api.UtPrimitiveModel import org.utbot.framework.plugin.api.WildcardTypeParameter import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.isArray @@ -50,7 +53,6 @@ import org.utbot.framework.plugin.api.util.isPrimitive import org.utbot.framework.plugin.api.util.isPrimitiveWrapper import org.utbot.framework.plugin.api.util.kClass import org.utbot.framework.plugin.api.util.voidClassId -import org.apache.commons.text.StringEscapeUtils //TODO rewrite using KtPsiFactory? internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrinterImpl()) : CgAbstractRenderer(context, printer) { @@ -242,6 +244,22 @@ internal class CgKotlinRenderer(context: CgContext, printer: CgPrinter = CgPrint } } + override fun visit(element: CgAllocateInitializedArray) { + val arrayModel = element.model + val elementsInLine = arrayElementsInLine(arrayModel.constModel) + + if (arrayModel.constModel is UtPrimitiveModel) { + val prefix = arrayModel.constModel.classId.name.toLowerCase() + print("${prefix}ArrayOf(") + arrayModel.renderElements(element.size, elementsInLine) + print(")") + } else { + print(getKotlinClassString(element.type)) + print("(${element.size})") + if (!element.elementType.isPrimitive) print(" { null }") + } + } + override fun visit(element: CgGetLength) { element.variable.accept(this) print(".size") 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 a88d485a4b..23cf060706 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 @@ -3,6 +3,7 @@ package org.utbot.framework.codegen.model.visitor import org.utbot.framework.codegen.model.tree.CgAbstractFieldAccess import org.utbot.framework.codegen.model.tree.CgAbstractMultilineComment 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 @@ -198,6 +199,7 @@ interface CgVisitor { // Array allocation fun visit(element: CgAllocateArray): R + fun visit(element: CgAllocateInitializedArray): R // Spread operator fun visit(element: CgSpread): 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 0a877aefff..c45e10e18b 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 @@ -15,6 +15,7 @@ import org.utbot.framework.codegen.model.constructor.builtin.iterablesDeepEquals import org.utbot.framework.codegen.model.constructor.builtin.mapsDeepEqualsMethodId import org.utbot.framework.codegen.model.constructor.builtin.setFieldMethodId import org.utbot.framework.codegen.model.constructor.builtin.setStaticFieldMethodId +import org.utbot.framework.codegen.model.constructor.builtin.streamsDeepEqualsMethodId 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.util.importIfNeeded @@ -42,6 +43,7 @@ internal fun ClassId.utilMethodById(id: MethodId, context: CgContext): String = deepEqualsMethodId -> deepEquals(codegenLanguage, mockFrameworkUsed, mockFramework) arraysDeepEqualsMethodId -> arraysDeepEquals(codegenLanguage) iterablesDeepEqualsMethodId -> iterablesDeepEquals(codegenLanguage) + streamsDeepEqualsMethodId -> streamsDeepEquals(codegenLanguage) mapsDeepEqualsMethodId -> mapsDeepEquals(codegenLanguage) hasCustomEqualsMethodId -> hasCustomEquals(codegenLanguage) getArrayLengthMethodId -> getArrayLength(codegenLanguage) @@ -70,8 +72,8 @@ fun getEnumConstantByName(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getEnumConstantByName(enumClass: Class<*>, name: String): Any? { - val fields: Array = enumClass.declaredFields + private fun getEnumConstantByName(enumClass: Class<*>, name: String): kotlin.Any? { + val fields: kotlin.Array = enumClass.declaredFields for (field in fields) { val fieldName = field.name if (field.isEnumConstant && fieldName == name) { @@ -91,7 +93,7 @@ fun getStaticFieldValue(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getStaticFieldValue(Class clazz, String fieldName) throws Exception { + private static Object getStaticFieldValue(Class clazz, String fieldName) throws IllegalAccessException, NoSuchFieldException { java.lang.reflect.Field field; Class originClass = clazz; do { @@ -114,7 +116,7 @@ fun getStaticFieldValue(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getStaticFieldValue(clazz: Class<*>, fieldName: String): Any? { + private fun getStaticFieldValue(clazz: Class<*>, fieldName: String): kotlin.Any? { var currentClass: Class<*>? = clazz var field: java.lang.reflect.Field do { @@ -141,7 +143,7 @@ fun getFieldValue(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getFieldValue(Object obj, String fieldName) throws Exception { + private static Object getFieldValue(Object obj, String fieldName) throws IllegalAccessException, NoSuchFieldException { Class clazz = obj.getClass(); java.lang.reflect.Field field; do { @@ -150,7 +152,7 @@ fun getFieldValue(language: CodegenLanguage): String = field.setAccessible(true); java.lang.reflect.Field modifiersField = java.lang.reflect.Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); - modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + modifiersField.setInt(field, field.getModifiers() & ~java.lang.reflect.Modifier.FINAL); return field.get(obj); } catch (NoSuchFieldException e) { @@ -164,7 +166,7 @@ fun getFieldValue(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getFieldValue(any: Any, fieldName: String): Any? { + private fun getFieldValue(any: kotlin.Any, fieldName: String): kotlin.Any? { var clazz: Class<*>? = any.javaClass var field: java.lang.reflect.Field do { @@ -191,7 +193,7 @@ fun setStaticField(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws Exception { + private static void setStaticField(Class clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { java.lang.reflect.Field field; do { @@ -214,7 +216,7 @@ fun setStaticField(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: Any?) { + private fun setStaticField(defaultClass: Class<*>, fieldName: String, fieldValue: kotlin.Any?) { var field: java.lang.reflect.Field? var clazz = defaultClass @@ -242,7 +244,7 @@ fun setField(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static void setField(Object object, String fieldName, Object fieldValue) throws Exception { + private static void setField(Object object, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Class clazz = object.getClass(); java.lang.reflect.Field field; @@ -266,7 +268,7 @@ fun setField(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun setField(any: Any, fieldName: String, fieldValue: Any?) { + private fun setField(any: kotlin.Any, fieldName: String, fieldValue: kotlin.Any?) { var clazz: Class<*> = any.javaClass var field: java.lang.reflect.Field? do { @@ -306,14 +308,18 @@ fun createArray(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun createArray(className: String, length: Int, vararg values: Any): Array { - val array: Any = java.lang.reflect.Array.newInstance(Class.forName(className), length) + private fun createArray( + className: String, + length: Int, + vararg values: kotlin.Any + ): kotlin.Array { + val array: kotlin.Any = java.lang.reflect.Array.newInstance(Class.forName(className), length) for (i in values.indices) { java.lang.reflect.Array.set(array, i, values[i]) } - return array as Array + return array as kotlin.Array } """ } @@ -323,7 +329,8 @@ fun createInstance(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object createInstance(String className) throws Exception { + private static Object createInstance(String className) + throws ClassNotFoundException, NoSuchMethodException, NoSuchFieldException, IllegalAccessException, InvocationTargetException { Class clazz = Class.forName(className); return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class.class) .invoke(getUnsafeInstance(), clazz); @@ -332,7 +339,7 @@ fun createInstance(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun createInstance(className: String): Any? { + private fun createInstance(className: String): kotlin.Any? { val clazz: Class<*> = Class.forName(className) return Class.forName("sun.misc.Unsafe").getDeclaredMethod("allocateInstance", Class::class.java) .invoke(getUnsafeInstance(), clazz) @@ -345,7 +352,7 @@ fun getUnsafeInstance(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private static Object getUnsafeInstance() throws Exception { + private static Object getUnsafeInstance() throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { java.lang.reflect.Field f = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe"); f.setAccessible(true); return f.get(null); @@ -354,7 +361,7 @@ fun getUnsafeInstance(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun getUnsafeInstance(): Any? { + private fun getUnsafeInstance(): kotlin.Any? { val f: java.lang.reflect.Field = Class.forName("sun.misc.Unsafe").getDeclaredField("theUnsafe") f.isAccessible = true return f[null] @@ -403,10 +410,10 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } private boolean deepEquals(Object o1, Object o2) { - return deepEquals(o1, o2, new HashSet<>()); + return deepEquals(o1, o2, new java.util.HashSet<>()); } - private boolean deepEquals(Object o1, Object o2, Set visited) { + private boolean deepEquals(Object o1, Object o2, java.util.Set visited) { visited.add(new FieldsPair(o1, o2)); if (o1 == o2) { @@ -428,16 +435,28 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew if (o2 instanceof Iterable) { return false; } + + if (o1 instanceof java.util.stream.Stream) { + if (!(o2 instanceof java.util.stream.Stream)) { + return false; + } + + return streamsDeepEquals((java.util.stream.Stream) o1, (java.util.stream.Stream) o2, visited); + } + + if (o2 instanceof java.util.stream.Stream) { + return false; + } - if (o1 instanceof Map) { - if (!(o2 instanceof Map)) { + if (o1 instanceof java.util.Map) { + if (!(o2 instanceof java.util.Map)) { return false; } - return mapsDeepEquals((Map) o1, (Map) o2, visited); + return mapsDeepEquals((java.util.Map) o1, (java.util.Map) o2, visited); } - if (o2 instanceof Map) { + if (o2 instanceof java.util.Map) { return false; } @@ -461,9 +480,9 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } // common classes without custom equals, use comparison by fields - final List fields = new ArrayList<>(); + final java.util.List fields = new java.util.ArrayList<>(); while (firstClass != Object.class) { - fields.addAll(Arrays.asList(firstClass.getDeclaredFields())); + fields.addAll(java.util.Arrays.asList(firstClass.getDeclaredFields())); // Interface should not appear here firstClass = firstClass.getSuperclass(); } @@ -490,26 +509,36 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } CodegenLanguage.KOTLIN -> { """ - private fun deepEquals(o1: Any?, o2: Any?): Boolean = deepEquals(o1, o2, hashSetOf()) + private fun deepEquals(o1: kotlin.Any?, o2: kotlin.Any?): Boolean = deepEquals(o1, o2, hashSetOf()) - private fun deepEquals(o1: Any?, o2: Any?, visited: MutableSet>): Boolean { + private fun deepEquals( + o1: kotlin.Any?, + o2: kotlin.Any?, + visited: kotlin.collections.MutableSet> + ): Boolean { visited += o1 to o2 if (o1 === o2) return true if (o1 == null || o2 == null) return false - if (o1 is Iterable<*>) { - return if (o2 !is Iterable<*>) false else iterablesDeepEquals(o1, o2, visited) + if (o1 is kotlin.collections.Iterable<*>) { + return if (o2 !is kotlin.collections.Iterable<*>) false else iterablesDeepEquals(o1, o2, visited) } - if (o2 is Iterable<*>) return false + if (o2 is kotlin.collections.Iterable<*>) return false + + if (o1 is java.util.stream.Stream<*>) { + return if (o2 !is java.util.stream.Stream<*>) false else streamsDeepEquals(o1, o2, visited) + } + + if (o2 is java.util.stream.Stream<*>) return false - if (o1 is Map<*, *>) { - return if (o2 !is Map<*, *>) false else mapsDeepEquals(o1, o2, visited) + if (o1 is kotlin.collections.Map<*, *>) { + return if (o2 !is kotlin.collections.Map<*, *>) false else mapsDeepEquals(o1, o2, visited) } - if (o2 is Map<*, *>) return false + if (o2 is kotlin.collections.Map<*, *>) return false var firstClass: Class<*> = o1.javaClass if (firstClass.isArray) { @@ -528,8 +557,8 @@ fun deepEquals(language: CodegenLanguage, mockFrameworkUsed: Boolean, mockFramew } // common classes without custom equals, use comparison by fields - val fields: MutableList = mutableListOf() - while (firstClass != Any::class.java) { + val fields: kotlin.collections.MutableList = mutableListOf() + while (firstClass != kotlin.Any::class.java) { fields += listOf(*firstClass.declaredFields) // Interface should not appear here firstClass = firstClass.superclass @@ -559,14 +588,14 @@ fun arraysDeepEquals(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean arraysDeepEquals(Object arr1, Object arr2, Set visited) { - final int length = Array.getLength(arr1); - if (length != Array.getLength(arr2)) { + private boolean arraysDeepEquals(Object arr1, Object arr2, java.util.Set visited) { + final int length = java.lang.reflect.Array.getLength(arr1); + if (length != java.lang.reflect.Array.getLength(arr2)) { return false; } for (int i = 0; i < length; i++) { - if (!deepEquals(Array.get(arr1, i), Array.get(arr2, i), visited)) { + if (!deepEquals(java.lang.reflect.Array.get(arr1, i), java.lang.reflect.Array.get(arr2, i), visited)) { return false; } } @@ -577,7 +606,11 @@ fun arraysDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun arraysDeepEquals(arr1: Any?, arr2: Any?, visited: MutableSet>): Boolean { + private fun arraysDeepEquals( + arr1: kotlin.Any?, + arr2: kotlin.Any?, + visited: kotlin.collections.MutableSet> + ): Boolean { val size = java.lang.reflect.Array.getLength(arr1) if (size != java.lang.reflect.Array.getLength(arr2)) return false @@ -597,9 +630,9 @@ fun iterablesDeepEquals(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean iterablesDeepEquals(Iterable i1, Iterable i2, Set visited) { - final Iterator firstIterator = i1.iterator(); - final Iterator secondIterator = i2.iterator(); + private boolean iterablesDeepEquals(Iterable i1, Iterable i2, java.util.Set visited) { + final java.util.Iterator firstIterator = i1.iterator(); + final java.util.Iterator secondIterator = i2.iterator(); while (firstIterator.hasNext() && secondIterator.hasNext()) { if (!deepEquals(firstIterator.next(), secondIterator.next(), visited)) { return false; @@ -616,7 +649,11 @@ fun iterablesDeepEquals(language: CodegenLanguage): String = } CodegenLanguage.KOTLIN -> { """ - private fun iterablesDeepEquals(i1: Iterable<*>, i2: Iterable<*>, visited: MutableSet>): Boolean { + private fun iterablesDeepEquals( + i1: Iterable<*>, + i2: Iterable<*>, + visited: kotlin.collections.MutableSet> + ): Boolean { val firstIterator = i1.iterator() val secondIterator = i2.iterator() while (firstIterator.hasNext() && secondIterator.hasNext()) { @@ -629,16 +666,64 @@ fun iterablesDeepEquals(language: CodegenLanguage): String = } } +fun streamsDeepEquals(language: CodegenLanguage): String = + when (language) { + CodegenLanguage.JAVA -> { + """ + private boolean streamsDeepEquals( + java.util.stream.Stream s1, + java.util.stream.Stream s2, + java.util.Set visited + ) { + final java.util.Iterator firstIterator = s1.iterator(); + final java.util.Iterator secondIterator = s2.iterator(); + while (firstIterator.hasNext() && secondIterator.hasNext()) { + if (!deepEquals(firstIterator.next(), secondIterator.next(), visited)) { + return false; + } + } + + if (firstIterator.hasNext()) { + return false; + } + + return !secondIterator.hasNext(); + } + """.trimIndent() + } + CodegenLanguage.KOTLIN -> { + """ + private fun streamsDeepEquals( + s1: java.util.stream.Stream<*>, + s2: java.util.stream.Stream<*>, + visited: kotlin.collections.MutableSet> + ): Boolean { + val firstIterator = s1.iterator() + val secondIterator = s2.iterator() + while (firstIterator.hasNext() && secondIterator.hasNext()) { + if (!deepEquals(firstIterator.next(), secondIterator.next(), visited)) return false + } + + return if (firstIterator.hasNext()) false else !secondIterator.hasNext() + } + """.trimIndent() + } + } + fun mapsDeepEquals(language: CodegenLanguage): String = when (language) { CodegenLanguage.JAVA -> { """ - private boolean mapsDeepEquals(Map m1, Map m2, Set visited) { - final Iterator> firstIterator = m1.entrySet().iterator(); - final Iterator> secondIterator = m2.entrySet().iterator(); + private boolean mapsDeepEquals( + java.util.Map m1, + java.util.Map m2, + java.util.Set visited + ) { + final java.util.Iterator> firstIterator = m1.entrySet().iterator(); + final java.util.Iterator> secondIterator = m2.entrySet().iterator(); while (firstIterator.hasNext() && secondIterator.hasNext()) { - final Map.Entry firstEntry = firstIterator.next(); - final Map.Entry secondEntry = secondIterator.next(); + final java.util.Map.Entry firstEntry = firstIterator.next(); + final java.util.Map.Entry secondEntry = secondIterator.next(); if (!deepEquals(firstEntry.getKey(), secondEntry.getKey(), visited)) { return false; @@ -662,7 +747,7 @@ fun mapsDeepEquals(language: CodegenLanguage): String = private fun mapsDeepEquals( m1: kotlin.collections.Map<*, *>, m2: kotlin.collections.Map<*, *>, - visited: MutableSet> + visited: kotlin.collections.MutableSet> ): Boolean { val firstIterator = m1.entries.iterator() val secondIterator = m2.entries.iterator() @@ -704,9 +789,9 @@ fun hasCustomEquals(language: CodegenLanguage): String = """ private fun hasCustomEquals(clazz: Class<*>): Boolean { var c = clazz - while (Any::class.java != c) { + while (kotlin.Any::class.java != c) { try { - c.getDeclaredMethod("equals", Any::class.java) + c.getDeclaredMethod("equals", kotlin.Any::class.java) return true } catch (e: Exception) { // Interface should not appear here @@ -724,12 +809,12 @@ fun getArrayLength(codegenLanguage: CodegenLanguage) = CodegenLanguage.JAVA -> """ private static int getArrayLength(Object arr) { - return Array.getLength(arr); + return java.lang.reflect.Array.getLength(arr); } """.trimIndent() CodegenLanguage.KOTLIN -> """ - private fun getArrayLength(arr: Any?): Int = java.lang.reflect.Array.getLength(arr) + private fun getArrayLength(arr: kotlin.Any?): Int = java.lang.reflect.Array.getLength(arr) """.trimIndent() } @@ -746,7 +831,7 @@ private fun ClassId.regularImportsByUtilMethod(id: MethodId, codegenLanguage: Co val fieldClassId = Field::class.id return when (id) { getUnsafeInstanceMethodId -> listOf(fieldClassId) - createInstanceMethodId -> listOf() + createInstanceMethodId -> listOf(java.lang.reflect.InvocationTargetException::class.id) createArrayMethodId -> listOf(java.lang.reflect.Array::class.id) setFieldMethodId -> listOf(fieldClassId, Modifier::class.id) setStaticFieldMethodId -> listOf(fieldClassId, Modifier::class.id) @@ -773,13 +858,17 @@ private fun ClassId.regularImportsByUtilMethod(id: MethodId, codegenLanguage: Co } iterablesDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Iterable::class.id, Iterator::class.id, Set::class.id) - CodegenLanguage.KOTLIN -> listOf() + CodegenLanguage.KOTLIN -> emptyList() + } + streamsDeepEqualsMethodId -> when (codegenLanguage) { + CodegenLanguage.JAVA -> listOf(java.util.stream.Stream::class.id, Set::class.id) + CodegenLanguage.KOTLIN -> emptyList() } mapsDeepEqualsMethodId -> when (codegenLanguage) { CodegenLanguage.JAVA -> listOf(Map::class.id, Iterator::class.id, Set::class.id) - CodegenLanguage.KOTLIN -> listOf() + CodegenLanguage.KOTLIN -> emptyList() } - hasCustomEqualsMethodId -> listOf() + hasCustomEqualsMethodId -> emptyList() getArrayLengthMethodId -> listOf(java.lang.reflect.Array::class.id) else -> error("Unknown util method for class $this: $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 260c82e1c5..d1452b2cef 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 @@ -45,6 +45,7 @@ import kotlin.reflect.KClass import org.mockito.Mockito import org.mockito.stubbing.Answer import org.objectweb.asm.Type +import org.utbot.common.withAccessibility /** * Constructs values (including mocks) from models. @@ -290,7 +291,9 @@ class MockValueConstructor( constructedObjects[model]?.let { return it } with(model) { - val elementClassId = classId.elementClassId!! + val elementClassId = classId.elementClassId ?: error( + "Provided incorrect UtArrayModel without elementClassId. ClassId: ${model.classId}, model: $model" + ) return when (elementClassId.jvmName) { "B" -> ByteArray(length) { primitive(constModel) }.apply { stores.forEach { (index, model) -> this[index] = primitive(model) } @@ -431,10 +434,18 @@ class MockValueConstructor( } private fun MethodId.call(args: List, instance: Any?): Any? = - method.invokeCatching(obj = instance, args = args).getOrThrow() + method.run { + withAccessibility { + invokeCatching(obj = instance, args = args).getOrThrow() + } + } private fun ConstructorId.call(args: List): Any? = - constructor.newInstance(*args.toTypedArray()) + constructor.run { + withAccessibility { + newInstance(*args.toTypedArray()) + } + } /** * Fetches primitive value from NutsModel to create array of primitives. diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt index c36307ad0f..29d4d51139 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/concrete/UtExecutionInstrumentation.kt @@ -214,8 +214,12 @@ object UtExecutionInstrumentation : Instrumentation { return UtTimeoutException(exception) } val instrs = traceHandler.computeInstructionList() - val isNested = instrs.first().callId != instrs.last().callId - return if (instrs.last().instructionData is ExplicitThrowInstruction) { + val isNested = if (instrs.isEmpty()) { + false + } else { + instrs.first().callId != instrs.last().callId + } + return if (instrs.isNotEmpty() && instrs.last().instructionData is ExplicitThrowInstruction) { UtExplicitlyThrownException(exception, isNested) } else { UtImplicitlyThrownException(exception, isNested) diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ConstructorAnalyzer.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ConstructorAnalyzer.kt index 5fcce11639..bfc5aa9b66 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ConstructorAnalyzer.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/modifications/ConstructorAnalyzer.kt @@ -4,7 +4,9 @@ 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.id +import org.utbot.framework.plugin.api.util.isArray import org.utbot.framework.plugin.api.util.isRefType +import org.utbot.framework.plugin.api.util.jClass import soot.Scene import soot.SootMethod import soot.Type @@ -54,12 +56,12 @@ class ConstructorAnalyzer { * Retrieves information about [constructorId] params and modified fields from Soot. */ fun analyze(constructorId: ConstructorId): ConstructorAssembleInfo { - setFields.clear() - affectedFields.clear() + val setFields = mutableSetOf() + val affectedFields = mutableSetOf() val sootConstructor = sootConstructor(constructorId) ?: error("Soot representation of $constructorId is not found.") - val params = analyze(sootConstructor) + val params = analyze(sootConstructor, setFields, affectedFields) return ConstructorAssembleInfo(constructorId, params, setFields, affectedFields) } @@ -106,24 +108,26 @@ class ConstructorAnalyzer { return jimpleLocal.name.first() != '$' } - private val setFields = mutableSetOf() - private val affectedFields = mutableSetOf() private val visitedConstructors = mutableSetOf() - private fun analyze(sootConstructor: SootMethod): Map { + private fun analyze( + sootConstructor: SootMethod, + setFields: MutableSet, + affectedFields: MutableSet, + ): Map { if (sootConstructor in visitedConstructors) { return emptyMap() } visitedConstructors.add(sootConstructor) val jimpleBody = retrieveJimpleBody(sootConstructor) ?: return emptyMap() - analyzeAssignments(jimpleBody) + analyzeAssignments(jimpleBody, setFields, affectedFields) val indexOfLocals = jimpleVariableIndices(jimpleBody) val indexedFields = indexToField(sootConstructor).toMutableMap() for (invocation in invocations(jimpleBody)) { - val invokedIndexedFields = analyze(invocation.method) + val invokedIndexedFields = analyze(invocation.method, setFields, affectedFields) for ((index, argument) in invocation.args.withIndex()) { val fieldId = invokedIndexedFields[index] ?: continue @@ -140,7 +144,11 @@ class ConstructorAnalyzer { * Analyze assignments if they are primitive and allow * to set a field into required value so on. */ - private fun analyzeAssignments(jimpleBody: JimpleBody) { + private fun analyzeAssignments( + jimpleBody: JimpleBody, + setFields: MutableSet, + affectedFields: MutableSet, + ) { for (assn in assignments(jimpleBody)) { val leftPart = assn.leftOp as? JInstanceFieldRef ?: continue @@ -242,7 +250,11 @@ class ConstructorAnalyzer { */ private fun getParameterType(type: ClassId): Type? = try { - if (type.isRefType) scene.getRefType(type.name) else scene.getType(type.name) + when { + type.isRefType -> scene.getRefType(type.name) + type.isArray -> scene.getType(type.jClass.canonicalName) + else -> scene.getType(type.name) + } } catch (e: Exception) { null } diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SootUtils.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SootUtils.kt index d040055de7..a4be349dce 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SootUtils.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/SootUtils.kt @@ -25,11 +25,17 @@ import org.utbot.engine.overrides.collections.UtHashMap import org.utbot.engine.overrides.collections.UtHashSet import org.utbot.engine.overrides.collections.UtLinkedList import org.utbot.engine.overrides.UtOverrideMock +import org.utbot.engine.overrides.collections.Collection +import org.utbot.engine.overrides.collections.List import org.utbot.engine.overrides.collections.UtGenericStorage import org.utbot.engine.overrides.collections.UtOptional import org.utbot.engine.overrides.collections.UtOptionalDouble import org.utbot.engine.overrides.collections.UtOptionalInt import org.utbot.engine.overrides.collections.UtOptionalLong +import org.utbot.engine.overrides.collections.AbstractCollection +import org.utbot.engine.overrides.stream.Arrays +import org.utbot.engine.overrides.stream.Stream +import org.utbot.engine.overrides.stream.UtStream import java.io.File import java.nio.file.Path import kotlin.reflect.KClass @@ -87,6 +93,7 @@ private fun addBasicClasses(vararg classes: KClass<*>) { } private val classesToLoad = arrayOf( + AbstractCollection::class, UtMock::class, UtOverrideMock::class, UtLogicMock::class, @@ -127,5 +134,11 @@ private val classesToLoad = arrayOf( UtNativeStringWrapper::class, UtString::class, UtStringBuilder::class, - UtStringBuffer::class + UtStringBuffer::class, + Stream::class, + Arrays::class, + Collection::class, + List::class, + UtStream::class, + UtStream.UtStreamIterator::class ) \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/UtBotTestCaseGenerator.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/UtBotTestCaseGenerator.kt index c686aaf07d..9c078dd7e2 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/UtBotTestCaseGenerator.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/api/UtBotTestCaseGenerator.kt @@ -48,6 +48,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.yield import mu.KotlinLogging +import org.utbot.engine.* import soot.Scene import soot.jimple.JimpleBody import soot.toolkits.graph.ExceptionalUnitGraph @@ -57,6 +58,7 @@ object UtBotTestCaseGenerator : TestCaseGenerator { private val logger = KotlinLogging.logger {} private val timeoutLogger = KotlinLogging.logger(logger.name + ".timeout") + lateinit var configureEngine: (UtBotSymbolicEngine) -> Unit lateinit var isCanceled: () -> Boolean //properties to save time on soot initialization @@ -70,8 +72,23 @@ object UtBotTestCaseGenerator : TestCaseGenerator { classpath: String?, dependencyPaths: String, isCanceled: () -> Boolean + ) = init( + buildDir, + classpath, + dependencyPaths, + configureEngine = {}, + isCanceled + ) + + fun init( + buildDir: Path, + classpath: String?, + dependencyPaths: String, + configureEngine: (UtBotSymbolicEngine) -> Unit, + isCanceled: () -> Boolean ) { this.isCanceled = isCanceled + this.configureEngine = configureEngine if (isCanceled()) return checkFrameworkDependencies(dependencyPaths) @@ -166,11 +183,28 @@ object UtBotTestCaseGenerator : TestCaseGenerator { chosenClassesToMockAlways: Set = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id }, executionTimeEstimator: ExecutionTimeEstimator = ExecutionTimeEstimator(utBotGenerationTimeoutInMillis, 1) ): Flow { + val engine = createSymbolicEngine( + controller, + method, + mockStrategy, + chosenClassesToMockAlways, + executionTimeEstimator + ) + return createDefaultFlow(engine) + } + + private fun createSymbolicEngine( + controller: EngineController, + method: UtMethod<*>, + mockStrategy: MockStrategyApi, + chosenClassesToMockAlways: Set, + executionTimeEstimator: ExecutionTimeEstimator + ): UtBotSymbolicEngine { // TODO: create classLoader from buildDir/classpath and migrate from UtMethod to MethodId? logger.debug("Starting symbolic execution for $method --$mockStrategy--") val graph = graph(method) - val engine = UtBotSymbolicEngine( + return UtBotSymbolicEngine( controller, method, graph, @@ -180,11 +214,17 @@ object UtBotTestCaseGenerator : TestCaseGenerator { chosenClassesToMockAlways = chosenClassesToMockAlways, solverTimeoutInMillis = executionTimeEstimator.updatedSolverCheckTimeoutMillis ) + } - return flowOf( - engine.traverse(), - engine.fuzzing(), - ).flattenConcat() + private fun createDefaultFlow(engine: UtBotSymbolicEngine): Flow { + var flow = engine.traverse() + if (UtSettings.useFuzzing) { + flow = flowOf( + engine.fuzzing(System.currentTimeMillis() + UtSettings.fuzzingTimeoutInMillis), + flow, + ).flattenConcat() + } + return flow } // CONFLUENCE:The+UtBot+Java+timeouts @@ -224,7 +264,8 @@ object UtBotTestCaseGenerator : TestCaseGenerator { methods: List>, mockStrategy: MockStrategyApi, chosenClassesToMockAlways: Set = Mocker.javaDefaultClasses.mapTo(mutableSetOf()) { it.id }, - methodsGenerationTimeout: Long = utBotGenerationTimeoutInMillis + methodsGenerationTimeout: Long = utBotGenerationTimeoutInMillis, + generate: (engine: UtBotSymbolicEngine) -> Flow = ::createDefaultFlow ): List { if (isCanceled()) return methods.map { UtTestCase(it) } @@ -246,13 +287,15 @@ object UtBotTestCaseGenerator : TestCaseGenerator { //yield one to yield() - generateAsync( + val engine: UtBotSymbolicEngine = createSymbolicEngine( controller, method, mockStrategy, chosenClassesToMockAlways, executionTimeEstimator - ).collect { + ).apply(configureEngine) + + generate(engine).collect { when (it) { is UtExecution -> method2executions.getValue(method) += it is UtError -> method2errors.getValue(method).merge(it.description, 1, Int::plus) @@ -342,7 +385,6 @@ object UtBotTestCaseGenerator : TestCaseGenerator { val executions = mutableListOf() val errors = mutableMapOf() - runIgnoringCancellationException { runBlockingWithCancellationPredicate(isCanceled) { generateAsync(EngineController(), method, mockStrategy).collect { @@ -382,7 +424,9 @@ object UtBotTestCaseGenerator : TestCaseGenerator { val signature = method.callable.signature val sootMethod = clazz.methods.singleOrNull { it.pureJavaSignature == signature } ?: error("No such $signature found") - + if (!sootMethod.canRetrieveBody()) { + error("No method body for $sootMethod found") + } val methodBody = sootMethod.jimpleBody() val graph = methodBody.graph() diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/GenerateTestsAndSarifReportFacade.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/GenerateTestsAndSarifReportFacade.kt new file mode 100644 index 0000000000..5673f5f630 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/GenerateTestsAndSarifReportFacade.kt @@ -0,0 +1,114 @@ +package org.utbot.framework.plugin.sarif + +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.NoStaticMocking +import org.utbot.framework.codegen.model.ModelBasedTestCodeGenerator +import org.utbot.framework.plugin.api.UtBotTestCaseGenerator +import org.utbot.framework.plugin.api.UtTestCase +import org.utbot.sarif.SarifReport +import org.utbot.sarif.SourceFindingStrategy +import org.utbot.summary.summarize +import java.io.File +import java.net.URLClassLoader +import java.nio.file.Path + +/** + * Facade for `generateTestsAndSarifReport` task/mojo. + * Stores common logic between gradle and maven plugins. + */ +class GenerateTestsAndSarifReportFacade( + val sarifProperties: SarifExtensionProvider, + val sourceFindingStrategy: SourceFindingStrategy +) { + + /** + * Generates tests and a SARIF report for the class [targetClass]. + * Requires withUtContext() { ... }. + */ + fun generateForClass( + targetClass: TargetClassWrapper, + workingDirectory: Path, + runtimeClasspath: String + ) { + initializeEngine(runtimeClasspath, workingDirectory) + + val testCases = generateTestCases(targetClass, workingDirectory) + val testClassBody = generateTestCode(targetClass, testCases) + targetClass.testsCodeFile.writeText(testClassBody) + + generateReport(targetClass, testCases, testClassBody, sourceFindingStrategy) + } + + companion object { + /** + * Merges all [sarifReports] into one large [mergedSarifReportFile] containing all the information. + * Prints a message about where the SARIF file is saved if [verbose] is true. + */ + fun mergeReports( + sarifReports: List, + mergedSarifReportFile: File, + verbose: Boolean = true + ) { + val mergedReport = SarifReport.mergeReports(sarifReports) + mergedSarifReportFile.writeText(mergedReport) + if (verbose) { + println("SARIF report was saved to \"${mergedSarifReportFile.path}\"") + println("You can open it using the VS Code extension \"Sarif Viewer\"") + } + } + } + + // internal + + private val dependencyPaths by lazy { + val thisClassLoader = this::class.java.classLoader as URLClassLoader + thisClassLoader.urLs.joinToString(File.pathSeparator) { it.path } + } + + private fun initializeEngine(classPath: String, workingDirectory: Path) { + UtBotTestCaseGenerator.init(workingDirectory, classPath, dependencyPaths) { false } + } + + private fun generateTestCases(targetClass: TargetClassWrapper, workingDirectory: Path): List = + UtBotTestCaseGenerator.generateForSeveralMethods( + targetClass.targetMethods(), + sarifProperties.mockStrategy, + sarifProperties.classesToMockAlways, + sarifProperties.generationTimeout + ).map { + it.summarize(targetClass.sourceCodeFile, workingDirectory) + } + + private fun generateTestCode(targetClass: TargetClassWrapper, testCases: List): String = + initializeCodeGenerator(targetClass) + .generateAsString(testCases, targetClass.testsCodeFile.nameWithoutExtension) + + private fun initializeCodeGenerator(targetClass: TargetClassWrapper) = + ModelBasedTestCodeGenerator().apply { + val isNoStaticMocking = sarifProperties.staticsMocking is NoStaticMocking + val isForceStaticMocking = sarifProperties.forceStaticMocking == ForceStaticMocking.FORCE + init( + classUnderTest = targetClass.classUnderTest.java, + testFramework = sarifProperties.testFramework, + mockFramework = sarifProperties.mockFramework, + staticsMocking = sarifProperties.staticsMocking, + forceStaticMocking = sarifProperties.forceStaticMocking, + generateWarningsForStaticMocking = isNoStaticMocking && isForceStaticMocking, + codegenLanguage = sarifProperties.codegenLanguage + ) + } + + /** + * Creates a SARIF report for the class [targetClass]. + * Saves the report to the file specified in [targetClass]. + */ + private fun generateReport( + targetClass: TargetClassWrapper, + testCases: List, + testClassBody: String, + sourceFinding: SourceFindingStrategy + ) { + val sarifReport = SarifReport(testCases, testClassBody, sourceFinding).createReport() + targetClass.sarifReportFile.writeText(sarifReport) + } +} \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/SarifExtensionProvider.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/SarifExtensionProvider.kt new file mode 100644 index 0000000000..46e65a514a --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/SarifExtensionProvider.kt @@ -0,0 +1,120 @@ +package org.utbot.framework.plugin.sarif + +import org.utbot.engine.Mocker +import org.utbot.framework.codegen.* +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi +import java.io.File + +/** + * Provides fields needed to create a SARIF report. + * Defines transform function for these fields. + */ +interface SarifExtensionProvider { + + /** + * Classes for which the SARIF report will be created. + */ + val targetClasses: List + + /** + * Absolute path to the root of the relative paths in the SARIF report. + */ + val projectRoot: File + + /** + * Relative path to the root of the generated tests. + */ + val generatedTestsRelativeRoot: String + + /** + * Relative path to the root of the SARIF reports. + */ + val sarifReportsRelativeRoot: String + + /** + * Mark the directory with generated tests as `test sources root` or not. + */ + val markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean + + val testFramework: TestFramework + + val mockFramework: MockFramework + + /** + * Maximum tests generation time for one class (in milliseconds). + */ + val generationTimeout: Long + + val codegenLanguage: CodegenLanguage + + val mockStrategy: MockStrategyApi + + val staticsMocking: StaticsMocking + + val forceStaticMocking: ForceStaticMocking + + /** + * Classes to force mocking theirs static methods and constructors. + * Contains user-specified classes and `Mocker.defaultSuperClassesToMockAlwaysNames`. + */ + val classesToMockAlways: Set + + // transform functions + + fun testFrameworkParse(testFramework: String): TestFramework = + when (testFramework.toLowerCase()) { + "junit4" -> Junit4 + "junit5" -> Junit5 + "testng" -> TestNg + else -> error("Parameter testFramework == '$testFramework', but it can take only 'junit4', 'junit5' or 'testng'") + } + + fun mockFrameworkParse(mockFramework: String): MockFramework = + when (mockFramework.toLowerCase()) { + "mockito" -> MockFramework.MOCKITO + else -> error("Parameter mockFramework == '$mockFramework', but it can take only 'mockito'") + } + + fun generationTimeoutParse(generationTimeout: Long): Long { + if (generationTimeout < 0) + error("Parameter generationTimeout == $generationTimeout, but it should be non-negative") + return generationTimeout + } + + fun codegenLanguageParse(codegenLanguage: String): CodegenLanguage = + when (codegenLanguage.toLowerCase()) { + "java" -> CodegenLanguage.JAVA + "kotlin" -> CodegenLanguage.KOTLIN + else -> error("Parameter codegenLanguage == '$codegenLanguage', but it can take only 'java' or 'kotlin'") + } + + fun mockStrategyParse(mockStrategy: String): MockStrategyApi = + when (mockStrategy.toLowerCase()) { + "no-mocks" -> MockStrategyApi.NO_MOCKS + "other-packages" -> MockStrategyApi.OTHER_PACKAGES + "other-classes" -> MockStrategyApi.OTHER_CLASSES + else -> error("Parameter mockStrategy == '$mockStrategy', but it can take only 'no-mocks', 'other-packages' or 'other-classes'") + } + + fun staticsMockingParse(staticsMocking: String): StaticsMocking = + when (staticsMocking.toLowerCase()) { + "do-not-mock-statics" -> NoStaticMocking + "mock-statics" -> MockitoStaticMocking + else -> error("Parameter staticsMocking == '$staticsMocking', but it can take only 'do-not-mock-statics' or 'mock-statics'") + } + + fun forceStaticMockingParse(forceStaticMocking: String): ForceStaticMocking = + when (forceStaticMocking.toLowerCase()) { + "force" -> ForceStaticMocking.FORCE + "do-not-force" -> ForceStaticMocking.DO_NOT_FORCE + else -> error("Parameter forceStaticMocking == '$forceStaticMocking', but it can take only 'force' or 'do-not-force'") + } + + fun classesToMockAlwaysParse(specifiedClasses: List): Set = + (Mocker.defaultSuperClassesToMockAlwaysNames + specifiedClasses).map { className -> + ClassId(className) + }.toSet() +} \ No newline at end of file diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/TargetClassWrapper.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/TargetClassWrapper.kt similarity index 94% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/TargetClassWrapper.kt rename to utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/TargetClassWrapper.kt index 87bc84f857..e0e51c2ed7 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/TargetClassWrapper.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/TargetClassWrapper.kt @@ -1,4 +1,4 @@ -package org.utbot.sarif.wrappers +package org.utbot.framework.plugin.sarif import org.utbot.framework.plugin.api.UtMethod import java.io.File diff --git a/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/util/ClassUtil.kt b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/util/ClassUtil.kt new file mode 100644 index 0000000000..b8bdf1144f --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/framework/plugin/sarif/util/ClassUtil.kt @@ -0,0 +1,69 @@ +package org.utbot.framework.plugin.sarif.util + +import org.utbot.common.PathUtil +import org.utbot.common.loadClassesFromDirectory +import org.utbot.common.tryLoadClass +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import java.io.File + +object ClassUtil { + + /** + * Finds all classes loaded by the [classLoader] and whose class files + * are located in the [classesDirectory]. Returns the names of the found classes. + * Does not return classes without declared methods or without a canonicalName. + */ + fun findAllDeclaredClasses( + classLoader: ClassLoader, + classesDirectory: File + ): List = + classLoader + .loadClassesFromDirectory(classesDirectory) + .filter { clazz -> + clazz.canonicalName != null && clazz.declaredMethods.isNotEmpty() + } + .map { clazz -> + clazz.canonicalName + } + + /** + * Finds the source code file in the [sourceCodeFiles] by the given [classFqn]. + * Tries to find the file by the information available to [classLoader] + * if the [classFqn] is not found in the [sourceCodeFiles]. + */ + fun findSourceCodeFile( + classFqn: String, + sourceCodeFiles: List, + classLoader: ClassLoader + ): File? = + sourceCodeFiles.firstOrNull { sourceCodeFile -> + val relativePath = "${PathUtil.classFqnToPath(classFqn)}.${sourceCodeFile.extension}" + sourceCodeFile.endsWith(File(relativePath)) + } ?: findSourceCodeFileByClass(classFqn, sourceCodeFiles, classLoader) + + // internal + + /** + * Fallback logic: called after a failure of [findSourceCodeFile]. + */ + private fun findSourceCodeFileByClass( + classFqn: String, + sourceCodeFiles: List, + classLoader: ClassLoader + ): File? { + val clazz = classLoader.tryLoadClass(classFqn) + ?: return null + val sourceFileName = withUtContext(UtContext(classLoader)) { + Instrumenter.computeSourceFileName(clazz) // finds the file name in bytecode + } ?: return null + val candidates = sourceCodeFiles.filter { sourceCodeFile -> + sourceCodeFile.endsWith(File(sourceFileName)) + } + return if (candidates.size == 1) + candidates.first() + else // we can't decide which file is needed + null + } +} \ No newline at end of file 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 b94fca37b5..c8778f8cf6 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 @@ -63,6 +63,8 @@ data class GeneratedSarif(val text: String) { fun hasCodeFlows(): Boolean = text.contains("codeFlows") fun codeFlowsIsNotEmpty(): Boolean = text.contains("threadFlows") + + fun contains(value: String): Boolean = text.contains(value) } fun compileClassAndGetClassPath(classNameToSource: Pair): Pair { diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt new file mode 100644 index 0000000000..89f9c21aaf --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FallbackModelProvider.kt @@ -0,0 +1,106 @@ +package org.utbot.fuzzer + +import org.utbot.engine.isPublic +import org.utbot.framework.concrete.UtModelConstructor +import org.utbot.framework.plugin.api.ClassId +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.UtExecutableCallModel +import org.utbot.framework.plugin.api.UtModel +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtStatementModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.executableId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.framework.plugin.api.util.isIterable +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.kClass +import org.utbot.fuzzer.providers.AbstractModelProvider +import java.util.* +import java.util.function.IntSupplier +import kotlin.collections.ArrayList +import kotlin.collections.HashMap +import kotlin.reflect.KClass + +/** + * Provides some simple default models of any class. + * + * Used as a fallback implementation until other providers cover every type. + */ +open class FallbackModelProvider( + private val idGenerator: IntSupplier +): AbstractModelProvider() { + + override fun toModel(classId: ClassId): UtModel { + return createModelByClassId(classId) + } + + fun toModel(klazz: KClass<*>): UtModel = createSimpleModelByKClass(klazz) + + private fun createModelByClassId(classId: ClassId): UtModel { + val modelConstructor = UtModelConstructor(IdentityHashMap()) + val defaultConstructor = classId.jClass.constructors.firstOrNull { + it.parameters.isEmpty() && it.isPublic + } + return when { + classId.isPrimitive -> + classId.defaultValueModel() + classId.isArray -> + UtArrayModel( + id = idGenerator.asInt, + classId, + length = 0, + classId.elementClassId!!.defaultValueModel(), + mutableMapOf() + ) + classId.isIterable -> { + @Suppress("RemoveRedundantQualifierName") // ArrayDeque must be taken from java, not from kotlin + val defaultInstance = when { + defaultConstructor != null -> defaultConstructor.newInstance() + classId.jClass.isAssignableFrom(java.util.ArrayList::class.java) -> ArrayList() + classId.jClass.isAssignableFrom(java.util.TreeSet::class.java) -> TreeSet() + classId.jClass.isAssignableFrom(java.util.HashMap::class.java) -> HashMap() + classId.jClass.isAssignableFrom(java.util.ArrayDeque::class.java) -> java.util.ArrayDeque() + classId.jClass.isAssignableFrom(java.util.BitSet::class.java) -> BitSet() + else -> null + } + if (defaultInstance != null) + modelConstructor.construct(defaultInstance, classId) + else + createSimpleModelByKClass(classId.kClass) + } + else -> + createSimpleModelByKClass(classId.kClass) + } + } + + private fun createSimpleModelByKClass(kclass: KClass<*>): UtModel { + val defaultConstructor = kclass.java.constructors.firstOrNull { + it.parameters.isEmpty() && it.isPublic // check constructor is public + } + return if (kclass.isAbstract) { // sealed class is abstract by itself + UtNullModel(kclass.java.id) + } else if (defaultConstructor != null) { + val chain = mutableListOf() + val model = UtAssembleModel( + id = idGenerator.asInt, + kclass.id, + kclass.id.toString(), + chain + ) + chain.add( + UtExecutableCallModel(model, defaultConstructor.executableId, listOf(), model) + ) + model + } else { + UtCompositeModel( + id = idGenerator.asInt, + kclass.id, + isMock = false + ) + } + } +} diff --git a/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt new file mode 100644 index 0000000000..9efdcb7ad4 --- /dev/null +++ b/utbot-framework/src/main/kotlin/org/utbot/fuzzer/FuzzerFunctions.kt @@ -0,0 +1,253 @@ +package org.utbot.fuzzer + +import org.utbot.framework.plugin.api.classId +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.charClassId +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 +import org.utbot.framework.plugin.api.util.stringClassId +import mu.KotlinLogging +import soot.BooleanType +import soot.ByteType +import soot.CharType +import soot.DoubleType +import soot.FloatType +import soot.IntType +import soot.Local +import soot.LongType +import soot.ShortType +import soot.Unit +import soot.Value +import soot.ValueBox +import soot.jimple.Constant +import soot.jimple.IntConstant +import soot.jimple.InvokeExpr +import soot.jimple.NullConstant +import soot.jimple.internal.AbstractSwitchStmt +import soot.jimple.internal.ImmediateBox +import soot.jimple.internal.JAssignStmt +import soot.jimple.internal.JCastExpr +import soot.jimple.internal.JEqExpr +import soot.jimple.internal.JGeExpr +import soot.jimple.internal.JGtExpr +import soot.jimple.internal.JIfStmt +import soot.jimple.internal.JLeExpr +import soot.jimple.internal.JLookupSwitchStmt +import soot.jimple.internal.JLtExpr +import soot.jimple.internal.JNeExpr +import soot.jimple.internal.JTableSwitchStmt +import soot.jimple.internal.JVirtualInvokeExpr +import soot.toolkits.graph.ExceptionalUnitGraph + +private val logger = KotlinLogging.logger {} + +/** + * Finds constant values in method body. + */ +fun collectConstantsForFuzzer(graph: ExceptionalUnitGraph): Set { + return graph.body.units.reversed().asSequence() + .filter { it is JIfStmt || it is JAssignStmt || it is AbstractSwitchStmt} + .flatMap { unit -> + unit.useBoxes.map { unit to it.value } + } + .filter { (_, value) -> + value is Constant || value is Local || value is JCastExpr || value is InvokeExpr + } + .flatMap { (unit, value) -> + sequenceOf( + ConstantsFromIfStatement, + ConstantsFromCast, + ConstantsFromSwitchCase, + BoundValuesForDoubleChecks, + StringConstant, + ).flatMap { finder -> + try { + finder.find(graph, unit, value) + } catch (e: Exception) { + logger.warn(e) { "Cannot process constant value of type '${value.type}}'" } + emptyList() + } + }.let { result -> + if (result.any()) result else { + ConstantsAsIs.find(graph, unit, value).asSequence() + } + } + }.toSet() +} + +private interface ConstantsFinder { + fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List +} + +private object ConstantsFromIfStatement: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (value !is Constant || (unit !is JIfStmt && unit !is JAssignStmt)) return emptyList() + + var useBoxes: List = emptyList() + var ifStatement: JIfStmt? = null + // simple if statement + if (unit is JIfStmt) { + useBoxes = unit.conditionBox.value.useBoxes.mapNotNull { (it as? ImmediateBox)?.value } + ifStatement = unit + } + // statement with double and long that consists of 2 units: + // 1. compare (result = local compare constant) + // 2. if result + if (unit is JAssignStmt) { + useBoxes = unit.rightOp.useBoxes.mapNotNull { (it as? ImmediateBox)?.value } + ifStatement = nextDirectUnit(graph, unit) as? JIfStmt + } + + /* + * It is acceptable to check different types in if statement like ```2 == 2L```. + * + * Because of that fuzzer tries to find out the correct type between local and constant. + * Constant should be converted into type of local var in such way that if-statement can be true. + */ + val valueIndex = useBoxes.indexOf(value) + if (useBoxes.size == 2 && valueIndex >= 0 && ifStatement != null) { + val exactValue = value.plainValue + val local = useBoxes[(valueIndex + 1) % 2] + var op = sootIfToFuzzedOp(ifStatement) + if (valueIndex == 0) { + op = op.reverseOrElse { it } + } + // Soot loads any integer type as an Int, + // therefore we try to guess target type using second value + // in the if statement + return listOfNotNull( + when (local.type) { + is CharType -> FuzzedConcreteValue(charClassId, (exactValue as Int).toChar(), op) + is BooleanType -> FuzzedConcreteValue(booleanClassId, (exactValue == 1), op) + is ByteType -> FuzzedConcreteValue(byteClassId, (exactValue as Int).toByte(), op) + is ShortType -> FuzzedConcreteValue(shortClassId, (exactValue as Int).toShort(), op) + is IntType -> FuzzedConcreteValue(intClassId, exactValue, op) + is LongType -> FuzzedConcreteValue(longClassId, exactValue, op) + is FloatType -> FuzzedConcreteValue(floatClassId, exactValue, op) + is DoubleType -> FuzzedConcreteValue(doubleClassId, exactValue, op) + else -> null + } + ) + } + return emptyList() + } + +} + +private object ConstantsFromCast: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (value !is JCastExpr) return emptyList() + + val next = nextDirectUnit(graph, unit) + if (next is JAssignStmt) { + val const = next.useBoxes.findFirstInstanceOf() + if (const != null) { + val op = (nextDirectUnit(graph, next) as? JIfStmt)?.let(::sootIfToFuzzedOp) ?: FuzzedOp.NONE + val exactValue = const.plainValue as Number + return listOfNotNull( + when (value.op.type) { + is ByteType -> FuzzedConcreteValue(byteClassId, exactValue.toByte(), op) + is ShortType -> FuzzedConcreteValue(shortClassId, exactValue.toShort(), op) + is IntType -> FuzzedConcreteValue(intClassId, exactValue.toInt(), op) + is FloatType -> FuzzedConcreteValue(floatClassId, exactValue.toFloat(), op) + else -> null + } + ) + } + } + return emptyList() + } + +} + +private object ConstantsFromSwitchCase: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (unit !is JTableSwitchStmt && unit !is JLookupSwitchStmt) return emptyList() + val result = mutableListOf() + if (unit is JTableSwitchStmt) { + for (i in unit.lowIndex..unit.highIndex) { + result.add(FuzzedConcreteValue(intClassId, i, FuzzedOp.EQ)) + } + } + if (unit is JLookupSwitchStmt) { + unit.lookupValues.asSequence().filterIsInstance().forEach { + result.add(FuzzedConcreteValue(intClassId, it.value, FuzzedOp.EQ)) + } + } + return result + } +} + +private object BoundValuesForDoubleChecks: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (value !is InvokeExpr) return emptyList() + if (value.method.declaringClass.name != "java.lang.Double") return emptyList() + return when (value.method.name) { + "isNaN", "isInfinite", "isFinite" -> listOf( + FuzzedConcreteValue(doubleClassId, Double.POSITIVE_INFINITY), + FuzzedConcreteValue(doubleClassId, Double.NaN), + FuzzedConcreteValue(doubleClassId, 0.0), + ) + else -> emptyList() + } + } + +} + +private object StringConstant: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (unit !is JAssignStmt || value !is JVirtualInvokeExpr) return emptyList() + // if string constant is called from String class let's pass it as modification + if (value.method.declaringClass.name == "java.lang.String") { + val stringConstantWasPassedAsArg = unit.useBoxes.findFirstInstanceOf()?.plainValue + if (stringConstantWasPassedAsArg != null) { + return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsArg, FuzzedOp.CH)) + } + val stringConstantWasPassedAsThis = graph.getPredsOf(unit) + ?.filterIsInstance() + ?.firstOrNull() + ?.useBoxes + ?.findFirstInstanceOf() + ?.plainValue + if (stringConstantWasPassedAsThis != null) { + return listOf(FuzzedConcreteValue(stringClassId, stringConstantWasPassedAsThis, FuzzedOp.CH)) + } + } + return emptyList() + } + +} + +private object ConstantsAsIs: ConstantsFinder { + override fun find(graph: ExceptionalUnitGraph, unit: Unit, value: Value): List { + if (value !is Constant || value is NullConstant) return emptyList() + return listOf(FuzzedConcreteValue(value.type.classId, value.plainValue)) + + } + +} + +private inline fun List.findFirstInstanceOf(): T? { + return map { it.value } + .filterIsInstance() + .firstOrNull() +} + +private val Constant.plainValue + get() = javaClass.getField("value")[this] + +private fun sootIfToFuzzedOp(unit: JIfStmt) = when (unit.condition) { + is JEqExpr -> FuzzedOp.NE + is JNeExpr -> FuzzedOp.EQ + is JGtExpr -> FuzzedOp.LE + is JGeExpr -> FuzzedOp.LT + is JLtExpr -> FuzzedOp.GE + is JLeExpr -> FuzzedOp.GT + else -> FuzzedOp.NONE +} + +private fun nextDirectUnit(graph: ExceptionalUnitGraph, unit: Unit): Unit? = graph.getSuccsOf(unit).takeIf { it.size == 1 }?.first() \ No newline at end of file diff --git a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt index 42e70cad2d..286476b075 100644 --- a/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt +++ b/utbot-framework/src/main/kotlin/org/utbot/sarif/SarifReport.kt @@ -254,10 +254,12 @@ class SarifReport( return null // searching needed method call + val publicMethodCallPattern = "$methodName(" + val privateMethodCallPattern = Regex("""$methodName.*\.invoke\(""") // using reflection val methodCallLineNumber = testsBodyLines .drop(testMethodStartsAt + 1) // for search after it .indexOfFirst { line -> - line.contains("$methodName(") + line.contains(publicMethodCallPattern) || line.contains(privateMethodCallPattern) } if (methodCallLineNumber == -1) return null diff --git a/utbot-framework/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java b/utbot-framework/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java index f9c3fedcbc..89ffcb5157 100644 --- a/utbot-framework/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java +++ b/utbot-framework/src/test/java/org/utbot/examples/manual/UtBotJavaApiTest.java @@ -13,6 +13,7 @@ import org.utbot.examples.manual.examples.DirectAccessExample; import org.utbot.examples.manual.examples.MultiMethodExample; import org.utbot.examples.manual.examples.ProvidedExample; +import org.utbot.examples.manual.examples.StringSwitchExample; import org.utbot.examples.manual.examples.Trivial; import org.utbot.examples.manual.examples.customer.B; import org.utbot.examples.manual.examples.customer.C; @@ -1244,6 +1245,64 @@ public void testOnObjectWithArrayOfComplexArrays() { compileClassFile(destinationClassName, snippet2); } + @Test + public void testFuzzingSimple() { + UtBotJavaApi.setStopConcreteExecutorOnExit(false); + + String classpath = getClassPath(StringSwitchExample.class); + String dependencyClassPath = getDependencyClassPath(); + + UtCompositeModel classUnderTestModel = modelFactory.produceCompositeModel( + classIdForType(StringSwitchExample.class) + ); + + Method methodUnderTest = getMethodByName(StringSwitchExample.class, "validate", String.class, int.class, int.class); + + IdentityHashMap models = modelFactory.produceAssembleModel( + methodUnderTest, + StringSwitchExample.class, + Collections.singletonList(classUnderTestModel) + ); + + EnvironmentModels methodState = new EnvironmentModels( + models.get(classUnderTestModel), + Arrays.asList(new UtPrimitiveModel("initial model"), new UtPrimitiveModel(-10), new UtPrimitiveModel(0)), + Collections.emptyMap() + ); + + TestMethodInfo methodInfo = new TestMethodInfo( + methodUnderTest, + methodState); + List utTestCases1 = UtBotJavaApi.fuzzingTestCases( + Collections.singletonList( + methodInfo + ), + StringSwitchExample.class, + classpath, + dependencyClassPath, + MockStrategyApi.OTHER_PACKAGES, + 3000L, + (type) -> { + if (int.class.equals(type) || Integer.class.equals(type)) { + return Arrays.asList(0, Integer.MIN_VALUE, Integer.MAX_VALUE); + } + return null; + } + ); + + String generate = UtBotJavaApi.generate( + Collections.singletonList(methodInfo), + utTestCases1, + destinationClassName, + classpath, + dependencyClassPath, + StringSwitchExample.class + ); + + Snippet snippet2 = new Snippet(CodegenLanguage.JAVA, generate); + compileClassFile(destinationClassName, snippet2); + } + @NotNull private String getClassPath(Class clazz) { return clazz.getProtectionDomain().getCodeSource().getLocation().getPath(); diff --git a/utbot-framework/src/test/java/org/utbot/examples/manual/examples/StringSwitchExample.java b/utbot-framework/src/test/java/org/utbot/examples/manual/examples/StringSwitchExample.java new file mode 100644 index 0000000000..aab811c3c2 --- /dev/null +++ b/utbot-framework/src/test/java/org/utbot/examples/manual/examples/StringSwitchExample.java @@ -0,0 +1,18 @@ +package org.utbot.examples.manual.examples; + +public class StringSwitchExample { + + public int validate(String value, int number, int defaultValue) { + switch (value) { + case "one": + return number > 1 ? number : -1; + case "two": + return number > 2 ? number : -2; + case "three": + return number > 3 ? number : -3; + default: + return defaultValue; + } + } + +} diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/AbstractTestCaseGeneratorTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/AbstractTestCaseGeneratorTest.kt index 61ce16c4c2..1ab652db79 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/AbstractTestCaseGeneratorTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/AbstractTestCaseGeneratorTest.kt @@ -67,6 +67,7 @@ import kotlin.reflect.KFunction4 import kotlin.reflect.KFunction5 import mu.KotlinLogging import org.junit.jupiter.api.Assertions.assertTrue +import org.utbot.framework.PathSelectorType val logger = KotlinLogging.logger {} @@ -87,10 +88,10 @@ abstract class AbstractTestCaseGeneratorTest( UtSettings.checkSolverTimeoutMillis = 0 UtSettings.checkNpeInNestedMethods = true UtSettings.checkNpeInNestedNotPrivateMethods = true - UtSettings.checkNpeForFinalFields = true UtSettings.substituteStaticsWithSymbolicVariable = true UtSettings.useAssembleModelGenerator = true UtSettings.saveRemainingStatesForConcreteExecution = false + UtSettings.useFuzzing = false } // checks paramsBefore and result @@ -1233,6 +1234,101 @@ abstract class AbstractTestCaseGeneratorTest( summaryDisplayNameChecks = summaryDisplayNameChecks ) + protected inline fun checkThisAndStaticsAfter( + method: KFunction1, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + summaryTextChecks: List<(List?) -> Boolean> = listOf(), + summaryNameChecks: List<(String?) -> Boolean> = listOf(), + summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + summaryTextChecks = summaryTextChecks, + summaryNameChecks = summaryNameChecks, + summaryDisplayNameChecks = summaryDisplayNameChecks + ) + + protected inline fun checkThisAndStaticsAfter( + method: KFunction2, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + summaryTextChecks: List<(List?) -> Boolean> = listOf(), + summaryNameChecks: List<(String?) -> Boolean> = listOf(), + summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + summaryTextChecks = summaryTextChecks, + summaryNameChecks = summaryNameChecks, + summaryDisplayNameChecks = summaryDisplayNameChecks + ) + + protected inline fun checkThisAndStaticsAfter( + method: KFunction3, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + summaryTextChecks: List<(List?) -> Boolean> = listOf(), + summaryNameChecks: List<(String?) -> Boolean> = listOf(), + summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + summaryTextChecks = summaryTextChecks, + summaryNameChecks = summaryNameChecks, + summaryDisplayNameChecks = summaryDisplayNameChecks + ) + + protected inline fun checkThisAndStaticsAfter( + method: KFunction4, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + summaryTextChecks: List<(List?) -> Boolean> = listOf(), + summaryNameChecks: List<(String?) -> Boolean> = listOf(), + summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + summaryTextChecks = summaryTextChecks, + summaryNameChecks = summaryNameChecks, + summaryDisplayNameChecks = summaryDisplayNameChecks + ) + + protected inline fun checkThisAndStaticsAfter( + method: KFunction5, + branches: ExecutionsNumberMatcher, + vararg matchers: (T, T1, T2, T3, T4, StaticsType, R?) -> Boolean, + coverage: CoverageMatcher = Full, + mockStrategy: MockStrategyApi = NO_MOCKS, + additionalDependencies: Array> = emptyArray(), + summaryTextChecks: List<(List?) -> Boolean> = listOf(), + summaryNameChecks: List<(String?) -> Boolean> = listOf(), + summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() + ) = internalCheck( + method, mockStrategy, branches, matchers, coverage, T::class, T1::class, T2::class, T3::class, T4::class, + arguments = ::withThisAndStaticsAfter, + additionalDependencies = additionalDependencies, + summaryTextChecks = summaryTextChecks, + summaryNameChecks = summaryNameChecks, + summaryDisplayNameChecks = summaryDisplayNameChecks + ) + // checks paramsBefore, staticsBefore and return value for static methods protected inline fun checkStaticsInStaticMethod( method: KFunction0, @@ -1512,7 +1608,7 @@ abstract class AbstractTestCaseGeneratorTest( summaryNameChecks: List<(String?) -> Boolean> = listOf(), summaryDisplayNameChecks: List<(String?) -> Boolean> = listOf() ) = internalCheck( - method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class , + method, mockStrategy, branches, matchers, coverage, T1::class, T2::class, T3::class, T4::class, arguments = ::withParamsMutationsAndResult, additionalDependencies = additionalDependencies, summaryTextChecks = summaryTextChecks, @@ -2468,7 +2564,7 @@ internal fun invokeMatcher(matcher: Function, params: List) = whe fun ge(count: Int) = ExecutionsNumberMatcher("ge $count") { it >= count } fun eq(count: Int) = ExecutionsNumberMatcher("eq $count") { it == count } fun between(bounds: IntRange) = ExecutionsNumberMatcher("$bounds") { it in bounds } -val ignoreExecutionsNumber = ExecutionsNumberMatcher("Do not calculate") { true } +val ignoreExecutionsNumber = ExecutionsNumberMatcher("Do not calculate") { it > 0 } fun atLeast(percents: Int) = AtLeast(percents) @@ -2523,13 +2619,27 @@ class AtLeast(percents: Int) : CoverageMatcher("at least $percents% coverage", object DoNotCalculate : CoverageMatcher("Do not calculate", { true }) +class FullWithAssumptions(assumeCallsNumber: Int) : CoverageMatcher( + "full coverage except failed assume calls", + { it.instructionCounter.let { it.covered >= it.total - assumeCallsNumber } } +) { + init { + require(assumeCallsNumber > 0) { + "Non-positive number of assume calls $assumeCallsNumber passed (for zero calls use Full coverage matcher" + } + } +} + // simple matchers fun withResult(ex: UtValueExecution<*>) = ex.paramsBefore + ex.evaluatedResult fun withException(ex: UtValueExecution<*>) = ex.paramsBefore + ex.returnValue fun withStaticsBefore(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsBefore + ex.evaluatedResult fun withStaticsAfter(ex: UtValueExecution<*>) = ex.paramsBefore + ex.staticsAfter + ex.evaluatedResult +fun withThisAndStaticsAfter(ex: UtValueExecution<*>) = listOf(ex.callerBefore) + ex.paramsBefore + ex.staticsAfter + ex.evaluatedResult fun withThisAndResult(ex: UtValueExecution<*>) = listOf(ex.callerBefore) + ex.paramsBefore + ex.evaluatedResult -fun withThisStaticsBeforeAndResult(ex: UtValueExecution<*>) = listOf(ex.callerBefore) + ex.paramsBefore + ex.staticsBefore + ex.evaluatedResult +fun withThisStaticsBeforeAndResult(ex: UtValueExecution<*>) = + listOf(ex.callerBefore) + ex.paramsBefore + ex.staticsBefore + ex.evaluatedResult + fun withThisAndException(ex: UtValueExecution<*>) = listOf(ex.callerBefore) + ex.paramsBefore + ex.returnValue fun withMocks(ex: UtValueExecution<*>) = ex.paramsBefore + listOf(ex.mocks) + ex.evaluatedResult fun withMocksAndInstrumentation(ex: UtValueExecution<*>) = @@ -2605,7 +2715,7 @@ fun keyMatch(keyStmt: List) = { summary: List? -> fun Map>.findByName(name: String) = entries.single { name == it.key.name }.value.value fun Map>.singleValue() = values.single().value -private typealias StaticsType = Map> +internal typealias StaticsType = Map> private typealias Mocks = List private typealias Instrumentation = List @@ -2639,7 +2749,10 @@ inline fun withoutMinimization(block: () -> T): T { } } -inline fun withSettingsFromTestFrameworkConfiguration(config: TestFrameworkConfiguration, block: () -> T): T { +inline fun withSettingsFromTestFrameworkConfiguration( + config: TestFrameworkConfiguration, + block: () -> T +): T { val substituteStaticsWithSymbolicVariable = UtSettings.substituteStaticsWithSymbolicVariable UtSettings.substituteStaticsWithSymbolicVariable = config.resetNonFinalFieldsAfterClinit try { @@ -2654,8 +2767,7 @@ inline fun withoutSubstituteStaticsWithSymbolicVariable(block: () -> T) { UtSettings.substituteStaticsWithSymbolicVariable = false try { block() - } - finally { + } finally { UtSettings.substituteStaticsWithSymbolicVariable = substituteStaticsWithSymbolicVariable } } @@ -2669,3 +2781,48 @@ inline fun withPushingStateFromPathSelectorForConcrete(block: () -> UtSettings.saveRemainingStatesForConcreteExecution = prev } } + +inline fun withTreatingOverflowAsError(block: () -> T): T { + val prev = UtSettings.treatOverflowAsError + UtSettings.treatOverflowAsError = true + try { + return block() + } finally { + UtSettings.treatOverflowAsError = prev + } +} + +inline fun withRewardModelPath(rewardModelPath: String, block: () -> T): T { + val prev = UtSettings.rewardModelPath + UtSettings.rewardModelPath = rewardModelPath + try { + return block() + } finally { + UtSettings.rewardModelPath = prev + } +} + +inline fun withPathSelectorType(pathSelectorType: PathSelectorType, block: () -> T): T { + val prev = UtSettings.pathSelectorType + UtSettings.pathSelectorType = pathSelectorType + try { + return block() + } finally { + UtSettings.pathSelectorType = prev + } +} + +inline fun withFeaturePath(featurePath: String, block: () -> T): T { + val prevFeaturePath = UtSettings.featurePath + val prevEnableFeatureProcess = UtSettings.enableFeatureProcess + + UtSettings.featurePath = featurePath + UtSettings.enableFeatureProcess = true + + try { + return block() + } finally { + UtSettings.featurePath = prevFeaturePath + UtSettings.enableFeatureProcess = prevEnableFeatureProcess + } +} diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/CodeTestCaseGeneratorTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/CodeTestCaseGeneratorTest.kt index 60a9d55d34..900aa0d896 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/CodeTestCaseGeneratorTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/CodeTestCaseGeneratorTest.kt @@ -35,7 +35,7 @@ import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor @ExtendWith(CodeTestCaseGeneratorTest.Companion.ReadRunningTestsNumberBeforeAllTestsCallback::class) abstract class CodeTestCaseGeneratorTest( private val testClass: KClass<*>, - private val testCodeGeneration: Boolean = true, + private var testCodeGeneration: Boolean = true, private val languagesLastStages: List = listOf( CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN) @@ -49,6 +49,17 @@ abstract class CodeTestCaseGeneratorTest( if (testCodeGeneration) testCases += testCase } + protected fun withEnabledTestingCodeGeneration(testCodeGeneration: Boolean, block: () -> Unit) { + val prev = this.testCodeGeneration + + try { + this.testCodeGeneration = testCodeGeneration + block() + } finally { + this.testCodeGeneration = prev + } + } + // save all generated test cases from current class to test code generation private fun addTestCase(pkg: Package) { if (testCodeGeneration) { diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt index 864a41da30..e7c660e426 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/ArrayOfArraysTest.kt @@ -266,4 +266,14 @@ internal class ArrayOfArraysTest : AbstractTestCaseGeneratorTest(testClass = Arr { valueBefore, valueAfter -> valueAfter.withIndex().all { it.value == valueBefore[it.index] + it.index } } ) } + + @Test + fun testArrayWithItselfAnAsElement() { + check( + ArrayOfArrays::arrayWithItselfAnAsElement, + eq(2), + coverage = atLeast(percents = 94) + // because of the assumption + ) + } } \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt index 255994e261..5d32297515 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/arrays/IntArrayBasicsTest.kt @@ -18,6 +18,18 @@ internal class IntArrayBasicsTest : AbstractTestCaseGeneratorTest( CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) ) ) { + @Test + fun testIntArrayWithAssumeOrExecuteConcretely() { + check( + IntArrayBasics::intArrayWithAssumeOrExecuteConcretely, + eq(4), + { x, n, r -> x > 0 && n < 20 && r?.size == 2 }, + { x, n, r -> x > 0 && n >= 20 && r?.size == 4 }, + { x, n, r -> x <= 0 && n < 20 && r?.size == 10 }, + { x, n, r -> x <= 0 && n >= 20 && r?.size == 20 }, + ) + } + @Test fun testInitArray() { checkWithException( diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt index 2b672c053c..fab796ada3 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/LinkedListsTest.kt @@ -133,7 +133,7 @@ internal class LinkedListsTest : AbstractTestCaseGeneratorTest( LinkedLists::peekLast, eq(3), { l, _ -> l == null }, - { l, r -> l != null && l.isEmpty() && r.isException() }, + { l, r -> l != null && (l.isEmpty() || l.last() == null) && r.isException() }, { l, r -> l != null && l.isNotEmpty() && r.getOrNull() == l.last() }, coverage = DoNotCalculate ) @@ -146,8 +146,8 @@ internal class LinkedListsTest : AbstractTestCaseGeneratorTest( eq(4), { l, _ -> l == null }, { l, r -> l != null && l.isEmpty() && r.isException() }, - { l, _ -> l != null && l.isNotEmpty() && l[0] == null }, - { l, r -> l != null && l.isNotEmpty() && r.getOrNull() == l[0] }, + { l, r -> l != null && l.isNotEmpty() && l[0] == null && r.isException() }, + { l, r -> l != null && l.isNotEmpty() && l[0] != null && r.getOrNull() == l[0] }, coverage = DoNotCalculate ) } diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt index 6a8988ecaa..445fbf914a 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListAlgorithmsTest.kt @@ -6,6 +6,7 @@ import org.utbot.examples.eq import org.utbot.framework.codegen.CodeGeneration import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Test +import org.utbot.examples.atLeast // TODO failed Kotlin compilation SAT-1332 class ListAlgorithmsTest : AbstractTestCaseGeneratorTest( @@ -23,10 +24,10 @@ class ListAlgorithmsTest : AbstractTestCaseGeneratorTest( ListAlgorithms::mergeListsInplace, eq(4), { a, b, r -> b.subList(0, b.size - 1).any { a.last() < it } && r != null && r == r.sorted() }, - { a, b, r -> a.subList(0, a.size - 1).any { b.last() <= it } && r != null && r == r.sorted() }, + { a, b, r -> (a.subList(0, a.size - 1).any { b.last() <= it } || a.any { ai -> b.any { ai < it } }) && r != null && r == r.sorted() }, { a, b, r -> a[0] < b[0] && r != null && r == r.sorted() }, { a, b, r -> a[0] >= b[0] && r != null && r == r.sorted() }, - coverage = DoNotCalculate + coverage = atLeast(94) ) } } \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListsTest.kt index 9fd7c277ce..feb5b8cdc4 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListsTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/ListsTest.kt @@ -5,6 +5,7 @@ import org.utbot.examples.DoNotCalculate import org.utbot.examples.eq import org.utbot.examples.ge import org.utbot.examples.ignoreExecutionsNumber +import org.utbot.examples.between import org.utbot.examples.isException import org.utbot.framework.codegen.CodeGeneration import org.utbot.framework.plugin.api.CodegenLanguage @@ -20,6 +21,16 @@ internal class ListsTest : AbstractTestCaseGeneratorTest( CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) ) ) { + @Test + fun testBigListFromParameters() { + check( + Lists::bigListFromParameters, + eq(1), + { list, r -> list.size == r && list.size == 11 }, + coverage = DoNotCalculate + ) + } + @Test fun testGetNonEmptyCollection() { check( @@ -104,7 +115,7 @@ internal class ListsTest : AbstractTestCaseGeneratorTest( fun removeElementsTest() { checkWithException( Lists::removeElements, - eq(7), + between(7..8), { list, _, _, r -> list == null && r.isException() }, { list, i, _, r -> list != null && i < 0 && r.isException() }, { list, i, _, r -> list != null && i >= 0 && list.size > i && list[i] == null && r.isException() }, diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt index 22c7166f17..45a78bea1c 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/MapValuesTest.kt @@ -92,9 +92,11 @@ class MapValuesTest : AbstractTestCaseGeneratorTest( fun testIteratorNext() { checkWithException( MapValues::iteratorNext, - eq(4), + between(3..4), { map, result -> map == null && result.isException() }, - { map, result -> map != null && map.values.isEmpty() && result.isException() }, + // We might lose this branch depending on the order of the exploration since + // we do not register wrappers, and, therefore, do not try to cover all of their branches + // { map, result -> map != null && map.values.isEmpty() && result.isException() }, { map, result -> map != null && map.values.first() == null && result.isException() }, // as map is LinkedHashmap by default this matcher would be correct { map, result -> map != null && map.values.isNotEmpty() && result.getOrNull() == map.values.first() }, diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt index 789ad690bb..0203e3ef6a 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/collections/OptionalsTest.kt @@ -482,4 +482,14 @@ class OptionalsTest : AbstractTestCaseGeneratorTest( coverage = DoNotCalculate ) } + + @Test + fun testOptionalOfPositive() { + check( + Optionals::optionalOfPositive, + eq(2), + { value, result -> value > 0 && result != null && result.isPresent && result.get() == value }, + { value, result -> value <= 0 && result != null && !result.isPresent } + ) + } } \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranchesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranchesTest.kt deleted file mode 100644 index ec12cd5440..0000000000 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranchesTest.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.utbot.examples.enums - -import org.utbot.examples.AbstractTestCaseGeneratorTest -import org.utbot.examples.eq -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test - -class ClassWithEnumInsideDifficultBranchesTest : AbstractTestCaseGeneratorTest(testClass = ClassWithEnumInsideDifficultBranches::class) { - @Test - @Disabled("TODO JIRA:1612") - fun testDifficultIfBranch() { - check( - ClassWithEnumInsideDifficultBranches::useEnumInDifficultIf, - eq(2), - { s, r -> s.equals("TRYIF", ignoreCase = true) && r == 1 }, - { s, r -> !s.equals("TRYIF", ignoreCase = true) && r == 2 } - ) - } -} \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt index c264da89ea..079bf1d01c 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/enums/ClassWithEnumTest.kt @@ -1,16 +1,197 @@ package org.utbot.examples.enums +import org.utbot.common.findField import org.utbot.examples.AbstractTestCaseGeneratorTest +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.enums.ClassWithEnum.StatusEnum.ERROR +import org.utbot.examples.enums.ClassWithEnum.StatusEnum.READY +import org.utbot.examples.eq +import org.utbot.examples.isException +import org.utbot.examples.withPushingStateFromPathSelectorForConcrete import org.utbot.examples.withoutConcrete +import org.utbot.framework.plugin.api.FieldId +import org.utbot.framework.plugin.api.util.id import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test class ClassWithEnumTest : AbstractTestCaseGeneratorTest(testClass = ClassWithEnum::class) { @Test - @Disabled("TODO JIRA:1611") fun testOrdinal() { withoutConcrete { checkAllCombinations(ClassWithEnum::useOrdinal) } } + + @Test + fun testGetter() { + check( + ClassWithEnum::useGetter, + eq(2), + { s, r -> s == null && r == -1 }, + { s, r -> s != null && r == 0 }, + ) + } + + @Test + fun testDifficultIfBranch() { + check( + ClassWithEnum::useEnumInDifficultIf, + eq(2), + { s, r -> s.equals("TRYIF", ignoreCase = true) && r == 1 }, + { s, r -> !s.equals("TRYIF", ignoreCase = true) && r == 2 }, + ) + } + + @Test + @Disabled("TODO JIRA:1686") + fun testNullParameter() { + check( + ClassWithEnum::nullEnumAsParameter, + eq(3), + { e, _ -> e == null }, + { e, r -> e == READY && r == 0 }, + { e, r -> e == ERROR && r == -1 }, + ) + } + + @Test + @Disabled("TODO JIRA:1686") + fun testNullField() { + checkWithException( + ClassWithEnum::nullField, + eq(3), + { e, r -> e == null && r.isException() }, + { e, r -> e == ERROR && r.isException() }, + { e, r -> e == READY && r.getOrNull()!! == 3 && READY.s.length == 3 }, + ) + } + + @Test + @Disabled("TODO JIRA:1686") + fun testChangeEnum() { + checkWithException( + ClassWithEnum::changeEnum, + eq(3), + { e, r -> e == null && r.isException() }, + { e, r -> e == READY && r.getOrNull()!! == ERROR.ordinal }, + { e, r -> e == ERROR && r.getOrNull()!! == READY.ordinal }, + ) + } + + @Test + fun testChangeMutableField() { + // TODO testing code generation for this method is disabled because we need to restore original field state + // should be enabled after solving JIRA:1648 + withEnabledTestingCodeGeneration(testCodeGeneration = false) { + checkWithException( + ClassWithEnum::changeMutableField, + eq(2), + { e, r -> e == READY && r.getOrNull()!! == 2 }, + { e, r -> (e == null || e == ERROR) && r.getOrNull()!! == -2 }, + ) + } + } + + @Test + @Disabled("TODO JIRA:1686") + fun testCheckName() { + check( + ClassWithEnum::checkName, + eq(3), + { s, _ -> s == null }, + { s, r -> s == READY.name && r == ERROR.name }, + { s, r -> s != READY.name && r == READY.name }, + ) + } + + @Test + fun testChangingStaticWithEnumInit() { + checkThisAndStaticsAfter( + ClassWithEnum::changingStaticWithEnumInit, + eq(1), + { t, staticsAfter, r -> + // for some reasons x is inaccessible + val x = t.javaClass.findField("x").get(t) as Int + + val y = staticsAfter[FieldId(ClassWithEnum.ClassWithStaticField::class.id, "y")]!!.value as Int + + val areStaticsCorrect = x == 1 && y == 11 + areStaticsCorrect && r == true + } + ) + } + + @Test + fun testEnumValues() { + checkStaticMethod( + ClassWithEnum.StatusEnum::values, + eq(1), + { r -> r.contentEquals(arrayOf(READY, ERROR)) }, + ) + } + + @Test + fun testFromCode() { + checkStaticMethod( + ClassWithEnum.StatusEnum::fromCode, + eq(3), + { code, r -> code == 10 && r == READY }, + { code, r -> code == -10 && r == ERROR }, + { code, r -> code !in setOf(10, -10) && r == null }, // IllegalArgumentException + ) + } + + @Test + fun testFromIsReady() { + checkStaticMethod( + ClassWithEnum.StatusEnum::fromIsReady, + eq(2), + { isFirst, r -> isFirst && r == READY }, + { isFirst, r -> !isFirst && r == ERROR }, + ) + } + + @Test + @Disabled("TODO JIRA:1450") + fun testPublicGetCodeMethod() { + checkWithThis( + ClassWithEnum.StatusEnum::publicGetCode, + eq(2), + { enumInstance, r -> enumInstance == READY && r == 10 }, + { enumInstance, r -> enumInstance == ERROR && r == -10 }, + coverage = DoNotCalculate + ) + } + + @Test + fun testImplementingInterfaceEnumInDifficultBranch() { + withPushingStateFromPathSelectorForConcrete { + check( + ClassWithEnum::implementingInterfaceEnumInDifficultBranch, + eq(2), + { s, r -> s.equals("SUCCESS", ignoreCase = true) && r == 0 }, + { s, r -> !s.equals("SUCCESS", ignoreCase = true) && r == 2 }, + ) + } + } + + @Test + fun testAffectSystemStaticAndUseInitEnumFromIt() { + check( + ClassWithEnum::affectSystemStaticAndInitEnumFromItAndReturnField, + eq(1), + { r -> r == true }, + coverage = DoNotCalculate + ) + } + + @Test + fun testAffectSystemStaticAndInitEnumFromItAndGetItFromEnumFun() { + check( + ClassWithEnum::affectSystemStaticAndInitEnumFromItAndGetItFromEnumFun, + eq(1), + { r -> r == true }, + coverage = DoNotCalculate + ) + } } \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt new file mode 100644 index 0000000000..ecd4a8976e --- /dev/null +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/lambda/SimpleLambdaExamplesTest.kt @@ -0,0 +1,31 @@ +package org.utbot.examples.lambda + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.utbot.examples.AbstractTestCaseGeneratorTest +import org.utbot.examples.eq +import org.utbot.examples.isException + +class SimpleLambdaExamplesTest : AbstractTestCaseGeneratorTest(testClass = SimpleLambdaExamples::class) { + @Test + fun testBiFunctionLambdaExample() { + checkWithException( + SimpleLambdaExamples::biFunctionLambdaExample, + eq(2), + { _, b, r -> b == 0 && r.isException() }, + { a, b, r -> b != 0 && r.getOrThrow() == a / b }, + ) + } + + @Test + @Disabled("TODO 0 executions https://github.com/UnitTestBot/UTBotJava/issues/192") + fun testChoosePredicate() { + check( + SimpleLambdaExamples::choosePredicate, + eq(2), + { b, r -> b && !r!!.test(null) && r.test(0) }, + { b, r -> !b && r!!.test(null) && !r.test(0) }, + // TODO coverage calculation fails https://github.com/UnitTestBot/UTBotJava/issues/192 + ) + } +} diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt index ba76a60f40..bd389a2210 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowAsErrorTest.kt @@ -1,5 +1,7 @@ package org.utbot.examples.math +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test import org.utbot.examples.AbstractTestCaseGeneratorTest import org.utbot.examples.AtLeast import org.utbot.examples.algorithms.Sort @@ -7,15 +9,11 @@ import org.utbot.examples.eq import org.utbot.examples.ignoreExecutionsNumber import org.utbot.examples.isException import org.utbot.examples.withSolverTimeoutInMillis -import org.utbot.framework.UtSettings +import org.utbot.examples.withTreatingOverflowAsError import org.utbot.framework.codegen.Compilation import org.utbot.framework.plugin.api.CodegenLanguage import kotlin.math.floor import kotlin.math.sqrt -import org.junit.jupiter.api.AfterAll -import org.junit.jupiter.api.BeforeAll -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test internal class OverflowAsErrorTest : AbstractTestCaseGeneratorTest( testClass = OverflowExamples::class, @@ -27,133 +25,140 @@ internal class OverflowAsErrorTest : AbstractTestCaseGeneratorTest( CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, Compilation), ) ) { - private val treatOverflowAsError = UtSettings.treatOverflowAsError - - @BeforeAll - fun beforeAll() { - UtSettings.treatOverflowAsError = true - } - - @AfterAll - fun afterAll() { - UtSettings.treatOverflowAsError = treatOverflowAsError - } - @Test fun testIntOverflow() { - checkWithException( - OverflowExamples::intOverflow, - eq(5), - { x, _, r -> x * x * x <= 0 && x > 0 && r.isException() }, // through overflow - { x, _, r -> x * x * x <= 0 && x > 0 && r.isException() }, // through overflow (2nd '*') - { x, _, r -> x * x * x >= 0 && x >= 0 && r.getOrNull() == 0 }, - { x, y, r -> x * x * x > 0 && x > 0 && y == 10 && r.getOrNull() == 1 }, - { x, y, r -> x * x * x > 0 && x > 0 && y != 10 && r.getOrNull() == 0 }, - coverage = AtLeast(90), - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::intOverflow, + eq(5), + { x, _, r -> x * x * x <= 0 && x > 0 && r.isException() }, // through overflow + { x, _, r -> x * x * x <= 0 && x > 0 && r.isException() }, // through overflow (2nd '*') + { x, _, r -> x * x * x >= 0 && x >= 0 && r.getOrNull() == 0 }, + { x, y, r -> x * x * x > 0 && x > 0 && y == 10 && r.getOrNull() == 1 }, + { x, y, r -> x * x * x > 0 && x > 0 && y != 10 && r.getOrNull() == 0 }, + coverage = AtLeast(90), + ) + } } @Test fun testByteAddOverflow() { - checkWithException( - OverflowExamples::byteAddOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { x, y, r -> - val negOverflow = ((x + y).toByte() >= 0 && x < 0 && y < 0) - val posOverflow = ((x + y).toByte() <= 0 && x > 0 && y > 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::byteAddOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x + y).toByte() >= 0 && x < 0 && y < 0) + val posOverflow = ((x + y).toByte() <= 0 && x > 0 && y > 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testByteSubOverflow() { - checkWithException( - OverflowExamples::byteSubOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { x, y, r -> - val negOverflow = ((x - y).toByte() >= 0 && x < 0 && y > 0) - val posOverflow = ((x - y).toByte() <= 0 && x > 0 && y < 0) - (negOverflow || posOverflow) && r.isException()}, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::byteSubOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x - y).toByte() >= 0 && x < 0 && y > 0) + val posOverflow = ((x - y).toByte() <= 0 && x > 0 && y < 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testByteMulOverflow() { - checkWithException( - OverflowExamples::byteMulOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { _, _, r -> r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::byteMulOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow + ) + } } @Test fun testShortAddOverflow() { - checkWithException( - OverflowExamples::shortAddOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { x, y, r -> - val negOverflow = ((x + y).toShort() >= 0 && x < 0 && y < 0) - val posOverflow = ((x + y).toShort() <= 0 && x > 0 && y > 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::shortAddOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x + y).toShort() >= 0 && x < 0 && y < 0) + val posOverflow = ((x + y).toShort() <= 0 && x > 0 && y > 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testShortSubOverflow() { - checkWithException( - OverflowExamples::shortSubOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { x, y, r -> - val negOverflow = ((x - y).toShort() >= 0 && x < 0 && y > 0) - val posOverflow = ((x - y).toShort() <= 0 && x > 0 && y < 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::shortSubOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x - y).toShort() >= 0 && x < 0 && y > 0) + val posOverflow = ((x - y).toShort() <= 0 && x > 0 && y < 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testShortMulOverflow() { - checkWithException( - OverflowExamples::shortMulOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { _, _, r -> r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::shortMulOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow + ) + } } @Test fun testIntAddOverflow() { - checkWithException( - OverflowExamples::intAddOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { x, y, r -> - val negOverflow = ((x + y) >= 0 && x < 0 && y < 0) - val posOverflow = ((x + y) <= 0 && x > 0 && y > 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::intAddOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x + y) >= 0 && x < 0 && y < 0) + val posOverflow = ((x + y) <= 0 && x > 0 && y > 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testIntSubOverflow() { - checkWithException( - OverflowExamples::intSubOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { x, y, r -> - val negOverflow = ((x - y) >= 0 && x < 0 && y > 0) - val posOverflow = ((x - y) <= 0 && x > 0 && y < 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::intSubOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x - y) >= 0 && x < 0 && y > 0) + val posOverflow = ((x - y) <= 0 && x > 0 && y < 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test @@ -162,92 +167,121 @@ internal class OverflowAsErrorTest : AbstractTestCaseGeneratorTest( // Reason: softConstraints, containing limits for Int values, hang solver. // With solver timeout softConstraints are dropped and hard constraints are SAT for overflow. withSolverTimeoutInMillis(timeoutInMillis = 1000) { - checkWithException( - OverflowExamples::intMulOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::intMulOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow + ) + } } } @Test fun testLongAddOverflow() { - checkWithException( - OverflowExamples::longAddOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { x, y, r -> - val negOverflow = ((x + y) >= 0 && x < 0 && y < 0) - val posOverflow = ((x + y) <= 0 && x > 0 && y > 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::longAddOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x + y) >= 0 && x < 0 && y < 0) + val posOverflow = ((x + y) <= 0 && x > 0 && y > 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test fun testLongSubOverflow() { - checkWithException( - OverflowExamples::longSubOverflow, - eq(2), - { _, _, r -> !r.isException()}, - { x, y, r -> - val negOverflow = ((x - y) >= 0 && x < 0 && y > 0) - val posOverflow = ((x - y) <= 0 && x > 0 && y < 0) - (negOverflow || posOverflow) && r.isException() - }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::longSubOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { x, y, r -> + val negOverflow = ((x - y) >= 0 && x < 0 && y > 0) + val posOverflow = ((x - y) <= 0 && x > 0 && y < 0) + (negOverflow || posOverflow) && r.isException() + }, // through overflow + ) + } } @Test + @Disabled("Flaky branch count mismatch (1 instead of 2)") fun testLongMulOverflow() { // This test has solver timeout. // Reason: softConstraints, containing limits for Int values, hang solver. // With solver timeout softConstraints are dropped and hard constraints are SAT for overflow. withSolverTimeoutInMillis(timeoutInMillis = 2000) { - checkWithException( - OverflowExamples::longMulOverflow, - eq(2), - { _, _, r -> !r.isException() }, - { _, _, r -> r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::longMulOverflow, + eq(2), + { _, _, r -> !r.isException() }, + { _, _, r -> r.isException() }, // through overflow + ) + } } } @Test fun testIncOverflow() { - checkWithException( - OverflowExamples::incOverflow, - eq(2), - { _, r -> !r.isException()}, - { _, r -> r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::incOverflow, + eq(2), + { _, r -> !r.isException() }, + { _, r -> r.isException() }, // through overflow + ) + } } @Test fun testIntCubeOverflow() { val sqrtIntMax = floor(sqrt(Int.MAX_VALUE.toDouble())).toInt() - checkWithException( - OverflowExamples::intCubeOverflow, - eq(3), - { _, r -> !r.isException()}, - // Can't use abs(x) below, because abs(Int.MIN_VALUE) == Int.MIN_VALUE. - // (Int.MAX_VALUE shr 16) is the border of square overflow and cube overflow. - // Int.MAX_VALUE.toDouble().pow(1/3.toDouble()) - { x, r -> (x > -sqrtIntMax && x < sqrtIntMax ) && r.isException() }, // through overflow - { x, r -> (x <= -sqrtIntMax || x >= sqrtIntMax) && r.isException() }, // through overflow - ) + withTreatingOverflowAsError { + checkWithException( + OverflowExamples::intCubeOverflow, + eq(3), + { _, r -> !r.isException() }, + // Can't use abs(x) below, because abs(Int.MIN_VALUE) == Int.MIN_VALUE. + // (Int.MAX_VALUE shr 16) is the border of square overflow and cube overflow. + // Int.MAX_VALUE.toDouble().pow(1/3.toDouble()) + { x, r -> (x > -sqrtIntMax && x < sqrtIntMax) && r.isException() }, // through overflow + { x, r -> (x <= -sqrtIntMax || x >= sqrtIntMax) && r.isException() }, // through overflow + ) + } } // Generated Kotlin code does not compile, so disabled for now @Test @Disabled fun testQuickSort() { - checkWithException( - Sort::quickSort, - ignoreExecutionsNumber, - { _, _, _, r -> !r.isException()}, - { _, _, _, r -> r.isException() }, // through overflow + withTreatingOverflowAsError { + checkWithException( + Sort::quickSort, + ignoreExecutionsNumber, + { _, _, _, r -> !r.isException() }, + { _, _, _, r -> r.isException() }, // through overflow + ) + } + } + + @Test + fun testIntOverflowWithoutError() { + check( + OverflowExamples::intOverflow, + eq(6), + { x, _, r -> x * x * x <= 0 && x <= 0 && r == 0 }, + { x, _, r -> x * x * x > 0 && x <= 0 && r == 0 }, // through overflow + { x, y, r -> x * x * x > 0 && x > 0 && y != 10 && r == 0 }, + { x, y, r -> x * x * x > 0 && x > 0 && y == 10 && r == 1 }, + { x, y, r -> x * x * x <= 0 && x > 0 && y != 20 && r == 0 }, // through overflow + { x, y, r -> x * x * x <= 0 && x > 0 && y == 20 && r == 2 } // through overflow ) } } diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowExamplesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowExamplesTest.kt deleted file mode 100644 index 71a60ac5f9..0000000000 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/math/OverflowExamplesTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.utbot.examples.math - -import org.utbot.examples.AbstractTestCaseGeneratorTest -import org.utbot.examples.eq -import org.junit.jupiter.api.Test - -internal class OverflowExamplesTest : AbstractTestCaseGeneratorTest(testClass = OverflowExamples::class) { - @Test - fun testIntOverflow() { - check( - OverflowExamples::intOverflow, - eq(6), - { x, _, r -> x * x * x <= 0 && x <= 0 && r == 0 }, - { x, _, r -> x * x * x > 0 && x <= 0 && r == 0 }, // through overflow - { x, y, r -> x * x * x > 0 && x > 0 && y != 10 && r == 0 }, - { x, y, r -> x * x * x > 0 && x > 0 && y == 10 && r == 1 }, - { x, y, r -> x * x * x <= 0 && x > 0 && y != 20 && r == 0 }, // through overflow - { x, y, r -> x * x * x <= 0 && x > 0 && y == 20 && r == 2 } // through overflow - ) - } -} diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt index 22a91c7fa0..59e552f95e 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/mixed/StaticInitializerExampleTest.kt @@ -1,10 +1,12 @@ package org.utbot.examples.mixed +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test import org.utbot.examples.AbstractTestCaseGeneratorTest import org.utbot.examples.StaticInitializerExample import org.utbot.examples.eq -import org.junit.jupiter.api.Test +@Disabled("Unknown build failure") internal class StaticInitializerExampleTest : AbstractTestCaseGeneratorTest(testClass = StaticInitializerExample::class) { @Test fun testPositive() { diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt index 3d00f6c570..a8aeb24a9c 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/mock/MockWithSideEffectExampleTest.kt @@ -8,7 +8,6 @@ import org.utbot.examples.isException import org.utbot.framework.plugin.api.MockStrategyApi import org.junit.Test -@Ignore internal class MockWithSideEffectExampleTest : AbstractTestCaseGeneratorTest(testClass = MockWithSideEffectExample::class) { @Test fun testSideEffect() { diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt index 5eacc42259..a99b6a41cc 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/natives/NativeExamplesTest.kt @@ -5,16 +5,20 @@ import org.utbot.examples.DoNotCalculate import org.utbot.examples.eq import org.utbot.examples.ge import org.junit.jupiter.api.Test +import org.utbot.examples.withSolverTimeoutInMillis internal class NativeExamplesTest : AbstractTestCaseGeneratorTest(testClass = NativeExamples::class) { @Test fun testFindAndPrintSum() { - check( - NativeExamples::findAndPrintSum, - ge(1), - coverage = DoNotCalculate, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + NativeExamples::findAndPrintSum, + ge(1), + coverage = DoNotCalculate, + ) + } } @Test diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/EnumsTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/objects/EnumsTest.kt deleted file mode 100644 index 09f10a080a..0000000000 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/EnumsTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.utbot.examples.objects - -import org.utbot.examples.AbstractTestCaseGeneratorTest -import org.utbot.examples.DoNotCalculate -import org.utbot.examples.eq -import org.utbot.examples.objects.SimpleEnum.FIRST -import org.utbot.examples.objects.SimpleEnum.SECOND -import org.junit.jupiter.api.Disabled -import org.junit.jupiter.api.Test - -internal class EnumsTest : AbstractTestCaseGeneratorTest(testClass = SimpleEnum::class) { - @Test - fun testEnumValues() { - checkStaticMethod( - SimpleEnum::values, - eq(1), - { r -> r.contentEquals(arrayOf(FIRST, SECOND)) }, - // TODO: loader MemoryClassLoader attempted duplicate class definition - coverage = DoNotCalculate - ) - } - - @Test - fun testFromCode() { - checkStaticMethod( - SimpleEnum::fromCode, - eq(3), - { code, r -> code == 1 && r == FIRST }, - { code, r -> code == 2 && r == SECOND }, - { code, r -> code !in 1..2 && r == null }, // IllegalArgumentException - // TODO: CallerImpl$Method cannot access a member of class SimpleEnum with modifiers "static" - coverage = DoNotCalculate - ) - } - - @Test - fun testFromIsFirst() { - checkStaticMethod( - SimpleEnum::fromIsFirst, - eq(2), - { isFirst, r -> isFirst && r == FIRST }, - { isFirst, r -> !isFirst && r == SECOND }, - // TODO: CallerImpl$Method cannot access a member of class SimpleEnum with modifiers "static" - coverage = DoNotCalculate - ) - } - - @Test - @Disabled("JIRA:1450") - fun testPublicGetCodeMethod() { - checkWithThis( - SimpleEnum::publicGetCode, - eq(2), - { enumInstance, r -> enumInstance == FIRST && r == 1 }, - { enumInstance, r -> enumInstance == SECOND && r == 2 }, - coverage = DoNotCalculate - ) - } -} \ No newline at end of file diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt index 5be33620bf..db84fb2da0 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/objects/ModelMinimizationExamplesTest.kt @@ -6,7 +6,6 @@ import org.utbot.examples.DoNotCalculate import org.utbot.examples.eq import org.junit.Test -@Ignore internal class ModelMinimizationExamplesTest : AbstractTestCaseGeneratorTest(testClass = ModelMinimizationExamples::class) { @Test fun singleValueComparisonTest() { diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt index 40f0e7c173..7b552ada8b 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/objects/SimpleClassMultiInstanceExampleTest.kt @@ -6,7 +6,6 @@ import org.utbot.examples.DoNotCalculate import org.utbot.examples.eq import org.junit.Test -@Ignore internal class SimpleClassMultiInstanceExampleTest : AbstractTestCaseGeneratorTest(testClass = SimpleClassMultiInstanceExample::class) { @Test fun singleObjectChangeTest() { diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt new file mode 100644 index 0000000000..5ab5a133b9 --- /dev/null +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/stream/BaseStreamExampleTest.kt @@ -0,0 +1,407 @@ +package org.utbot.examples.stream + +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.utbot.examples.AbstractTestCaseGeneratorTest +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.Full +import org.utbot.examples.FullWithAssumptions +import org.utbot.examples.StaticsType +import org.utbot.examples.eq +import org.utbot.examples.ignoreExecutionsNumber +import org.utbot.examples.isException +import org.utbot.examples.withoutConcrete +import org.utbot.framework.codegen.CodeGeneration +import org.utbot.framework.plugin.api.CodegenLanguage +import java.util.Optional +import java.util.stream.Stream +import kotlin.streams.toList + +// TODO 1 instruction is always uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 +// TODO failed Kotlin compilation (generics) JIRA:1332 +class BaseStreamExampleTest : AbstractTestCaseGeneratorTest( + testClass = BaseStreamExample::class, + testCodeGeneration = true, + languagePipelines = listOf( + CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), + CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, CodeGeneration) + ) +) { + @Test + fun testReturningStreamExample() { + withoutConcrete { + check( + BaseStreamExample::returningStreamExample, + eq(2), + // NOTE: the order of the matchers is important because Stream could be used only once + { c, r -> c.isNotEmpty() && c == r!!.toList() }, + { c, r -> c.isEmpty() && c == r!!.toList() }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + + @Test + fun testReturningStreamAsParameterExample() { + withoutConcrete { + check( + BaseStreamExample::returningStreamAsParameterExample, + eq(1), + { s, r -> s != null && s.toList() == r!!.toList() }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + + @Test + fun testFilterExample() { + check( + BaseStreamExample::filterExample, + ignoreExecutionsNumber, + { c, r -> null !in c && r == false }, + { c, r -> null in c && r == true }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testMapExample() { + checkWithException( + BaseStreamExample::mapExample, + eq(2), + { c, r -> null in c && r.isException() }, + { c, r -> r.getOrThrow().contentEquals(c.map { it * 2 }.toTypedArray()) }, + coverage = DoNotCalculate + ) + } + + @Test + fun testFlatMapExample() { + check( + BaseStreamExample::flatMapExample, + ignoreExecutionsNumber, + { c, r -> r.contentEquals(c.flatMap { listOf(it, it) }.toTypedArray()) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testDistinctExample() { + check( + BaseStreamExample::distinctExample, + ignoreExecutionsNumber, + { c, r -> c == c.distinct() && r == false }, + { c, r -> c != c.distinct() && r == true }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + @Tag("slow") + // TODO slow sorting https://github.com/UnitTestBot/UTBotJava/issues/188 + fun testSortedExample() { + check( + BaseStreamExample::sortedExample, + ignoreExecutionsNumber, + { c, r -> c.last() < c.first() && r!!.asSequence().isSorted() }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testPeekExample() { + checkThisAndStaticsAfter( + BaseStreamExample::peekExample, + ignoreExecutionsNumber, + *streamConsumerStaticsMatchers, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testLimitExample() { + check( + BaseStreamExample::limitExample, + ignoreExecutionsNumber, + { c, r -> c.size <= 5 && c.toTypedArray().contentEquals(r) }, + { c, r -> c.size > 5 && c.take(5).toTypedArray().contentEquals(r) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testSkipExample() { + check( + BaseStreamExample::skipExample, + ignoreExecutionsNumber, + { c, r -> c.size > 5 && c.drop(5).toTypedArray().contentEquals(r) }, + { c, r -> c.size <= 5 && r!!.isEmpty() }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testForEachExample() { + checkThisAndStaticsAfter( + BaseStreamExample::forEachExample, + eq(2), + *streamConsumerStaticsMatchers, + coverage = DoNotCalculate + ) + } + + @Test + fun testToArrayExample() { + check( + BaseStreamExample::toArrayExample, + ignoreExecutionsNumber, + { c, r -> c.toTypedArray().contentEquals(r) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testReduceExample() { + check( + BaseStreamExample::reduceExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == 42 }, + { c, r -> c.isNotEmpty() && r == c.sum() + 42 }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testOptionalReduceExample() { + checkWithException( + BaseStreamExample::optionalReduceExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r.getOrThrow() == Optional.empty() }, + { c, r -> c.isNotEmpty() && c.single() == null && r.isException() }, + { c, r -> c.isNotEmpty() && r.getOrThrow() == Optional.of(c.sum()) }, + coverage = DoNotCalculate // TODO 2 instructions are uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 + ) + } + + @Test + fun testComplexReduceExample() { + check( + BaseStreamExample::complexReduceExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && c.sumOf { it.toDouble() } + 42.0 == r }, + { c: List, r -> c.isNotEmpty() && c.sumOf { it?.toDouble() ?: 0.0 } + 42.0 == r }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + @Disabled("TODO zero executions https://github.com/UnitTestBot/UTBotJava/issues/207") + fun testCollectorExample() { + check( + BaseStreamExample::collectorExample, + ignoreExecutionsNumber, + { c, r -> c.toSet() == r }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testCollectExample() { + checkWithException( + BaseStreamExample::collectExample, + ignoreExecutionsNumber, // 3 executions instead of 2 expected + { c, r -> null in c && r.isException() }, + { c, r -> null !in c && c.sum() == r.getOrThrow() }, + coverage = DoNotCalculate // TODO 2 instructions are uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 + ) + } + + @Test + fun testMinExample() { + checkWithException( + BaseStreamExample::minExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r.getOrThrow() == Optional.empty() }, + { c, r -> c.isNotEmpty() && c.all { it == null } && r.isException() }, + { c, r -> c.isNotEmpty() && r.getOrThrow() == Optional.of(c.minOrNull()!!) }, + coverage = DoNotCalculate // TODO 2 instructions are uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 + ) + } + + @Test + fun testMaxExample() { + checkWithException( + BaseStreamExample::maxExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r.getOrThrow() == Optional.empty() }, + { c, r -> c.isNotEmpty() && c.all { it == null } && r.isException() }, + { c, r -> c.isNotEmpty() && r.getOrThrow() == Optional.of(c.maxOrNull()!!) }, + coverage = DoNotCalculate // TODO 2 instructions are uncovered https://github.com/UnitTestBot/UTBotJava/issues/193 + ) + } + + @Test + fun testCountExample() { + check( + BaseStreamExample::countExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == 0L }, + { c, r -> c.isNotEmpty() && c.size.toLong() == r }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testAnyMatchExample() { + check( + BaseStreamExample::anyMatchExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == false }, + { c, r -> c.isNotEmpty() && c.all { it == null } && r == true }, + { c, r -> c.isNotEmpty() && c.first() != null && c.last() == null && r == true }, + { c, r -> c.isNotEmpty() && c.first() == null && c.last() != null && r == true }, + { c, r -> c.isNotEmpty() && c.none { it == null } && r == false }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testAllMatchExample() { + check( + BaseStreamExample::allMatchExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == true }, + { c, r -> c.isNotEmpty() && c.all { it == null } && r == true }, + { c, r -> c.isNotEmpty() && c.first() != null && c.last() == null && r == false }, + { c, r -> c.isNotEmpty() && c.first() == null && c.last() != null && r == false }, + { c, r -> c.isNotEmpty() && c.none { it == null } && r == false }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testNoneMatchExample() { + check( + BaseStreamExample::noneMatchExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == true }, + { c, r -> c.isNotEmpty() && c.all { it == null } && r == false }, + { c, r -> c.isNotEmpty() && c.first() != null && c.last() == null && r == false }, + { c, r -> c.isNotEmpty() && c.first() == null && c.last() != null && r == false }, + { c, r -> c.isNotEmpty() && c.none { it == null } && r == true }, + coverage = FullWithAssumptions(assumeCallsNumber = 2) + ) + } + + @Test + fun testFindFirstExample() { + checkWithException( + BaseStreamExample::findFirstExample, + eq(3), + { c, r -> c.isEmpty() && r.getOrThrow() == Optional.empty() }, + { c: List, r -> c.isNotEmpty() && c.first() == null && r.isException() }, + { c, r -> c.isNotEmpty() && r.getOrThrow() == Optional.of(c.first()) }, + coverage = DoNotCalculate + ) + } + + @Test + fun testIteratorExample() { + checkWithException( + BaseStreamExample::iteratorSumExample, + eq(2), + { c, r -> null in c && r.isException() }, + { c, r -> null !in c && r.getOrThrow() == c.sum() }, + coverage = DoNotCalculate + ) + } + + @Test + fun testStreamOfExample() { + withoutConcrete { + check( + BaseStreamExample::streamOfExample, + ignoreExecutionsNumber, + // NOTE: the order of the matchers is important because Stream could be used only once + { c, r -> c.isNotEmpty() && c.contentEquals(r!!.toArray()) }, + { c, r -> c.isEmpty() && Stream.empty().toArray().contentEquals(r!!.toArray()) }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + } + + @Test + fun testClosedStreamExample() { + checkWithException( + BaseStreamExample::closedStreamExample, + eq(1), + { _, r -> r.isException() }, + coverage = DoNotCalculate + ) + } + + @Test + @Disabled("TODO unsat type constraints https://github.com/UnitTestBot/UTBotJava/issues/253") + fun testCustomCollectionStreamExample() { + check( + BaseStreamExample::customCollectionStreamExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == 0L }, + { c, r -> c.isNotEmpty() && c.size.toLong() == r }, + coverage = DoNotCalculate + ) + } + + @Test + fun testAnyCollectionStreamExample() { + check( + BaseStreamExample::anyCollectionStreamExample, + ignoreExecutionsNumber, + { c, r -> c.isEmpty() && r == 0L }, + { c, r -> c.isNotEmpty() && c.size.toLong() == r }, + coverage = FullWithAssumptions(assumeCallsNumber = 1) + ) + } + + @Test + fun testGenerateExample() { + check( + BaseStreamExample::generateExample, + ignoreExecutionsNumber, + { r -> r!!.contentEquals(Array(10) { 42 }) }, + coverage = Full + ) + } + + @Test + fun testIterateExample() { + check( + BaseStreamExample::iterateExample, + ignoreExecutionsNumber, + { r -> r!!.contentEquals(Array(10) { i -> 42 + i }) }, + coverage = Full + ) + } + + @Test + fun testConcatExample() { + check( + BaseStreamExample::concatExample, + ignoreExecutionsNumber, + { r -> r!!.contentEquals(Array(10) { 42 } + Array(10) { i -> 42 + i }) }, + coverage = Full + ) + } + + private val streamConsumerStaticsMatchers = arrayOf( + { _: BaseStreamExample, c: List, _: StaticsType, _: Int? -> null in c }, + { _: BaseStreamExample, c: List, statics: StaticsType, r: Int? -> + val x = statics.values.single().value as Int + + r!! + c.sumOf { it ?: 0 } == x + } + ) +} + +private fun > Sequence.isSorted(): Boolean = zipWithNext { a, b -> a <= b }.all { it } diff --git a/utbot-framework/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt b/utbot-framework/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt index de59143346..196b63e19f 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/examples/strings/StringExamplesTest.kt @@ -15,6 +15,7 @@ import org.utbot.framework.codegen.CodeGeneration import org.utbot.framework.plugin.api.CodegenLanguage import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test +import org.utbot.examples.withSolverTimeoutInMillis internal class StringExamplesTest : AbstractTestCaseGeneratorTest( testClass = StringExamples::class, @@ -25,14 +26,16 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( ) ) { @Test - @Disabled("Sometimes it freezes the execution for several hours JIRA:1453") fun testByteToString() { - check( - StringExamples::byteToString, - eq(2), - { a, b, r -> a > b && r == a.toString() }, - { a, b, r -> a <= b && r == b.toString() }, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + StringExamples::byteToString, + eq(2), + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } } @Test @@ -49,34 +52,43 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( @Test fun testShortToString() { - check( - StringExamples::shortToString, - eq(2), - { a, b, r -> a > b && r == a.toString() }, - { a, b, r -> a <= b && r == b.toString() }, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + StringExamples::shortToString, + eq(2), + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } } @Test fun testIntToString() { - check( - StringExamples::intToString, - ignoreExecutionsNumber, - { a, b, r -> a > b && r == a.toString() }, - { a, b, r -> a <= b && r == b.toString() }, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + StringExamples::intToString, + ignoreExecutionsNumber, + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } } @Test fun testLongToString() { - check( - StringExamples::longToString, - ignoreExecutionsNumber, - { a, b, r -> a > b && r == a.toString() }, - { a, b, r -> a <= b && r == b.toString() }, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + StringExamples::longToString, + ignoreExecutionsNumber, + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } } @Test @@ -104,12 +116,15 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( @Test fun testCharToString() { - check( - StringExamples::charToString, - eq(2), - { a, b, r -> a > b && r == a.toString() }, - { a, b, r -> a <= b && r == b.toString() }, - ) + // TODO related to the https://github.com/UnitTestBot/UTBotJava/issues/131 + withSolverTimeoutInMillis(5000) { + check( + StringExamples::charToString, + eq(2), + { a, b, r -> a > b && r == a.toString() }, + { a, b, r -> a <= b && r == b.toString() }, + ) + } } @@ -328,7 +343,7 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( fun testSubstringWithEndIndex() { checkWithException( StringExamples::substringWithEndIndex, - between(6..8), + ignoreExecutionsNumber, { s, _, _, r -> s == null && r.isException() }, { s, b, e, r -> s != null && b < 0 || e > s.length || b > e && r.isException() }, { s, b, e, r -> r.getOrThrow() == s.substring(b, e) && s.substring(b, e) != "password" }, @@ -346,7 +361,7 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( fun testSubstringWithEndIndexNotEqual() { checkWithException( StringExamples::substringWithEndIndexNotEqual, - between(3..4), + ignoreExecutionsNumber, { s, _, r -> s == null && r.isException() }, { s, e, r -> s != null && e < 1 || e > s.length && r.isException() }, { s, e, r -> s != null && r.getOrThrow() == s.substring(1, e) }, @@ -574,4 +589,14 @@ internal class StringExamplesTest : AbstractTestCaseGeneratorTest( ) } } + + @Test + fun testListToString() { + check( + StringExamples::listToString, + eq(1), + { r -> r == "[a, b, c]"}, + coverage = DoNotCalculate + ) + } } diff --git a/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt b/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt index dc9e0bacab..fad4ff0334 100644 --- a/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt +++ b/utbot-framework/src/test/kotlin/org/utbot/sarif/SarifReportTest.kt @@ -17,7 +17,7 @@ class SarifReportTest { val actualReport = SarifReport( testCases = listOf(), generatedTestsCode = "", - SourceFindingStrategyDefault("", "", "", "") + sourceFindingEmpty ).createReport() assert(actualReport.isNotEmpty()) @@ -28,7 +28,7 @@ class SarifReportTest { val sarif = SarifReport( testCases = listOf(testCase), generatedTestsCode = "", - SourceFindingStrategyDefault("", "", "", "") + sourceFindingEmpty ).createReport().toSarif() assert(sarif.runs.first().results.isEmpty()) @@ -58,7 +58,7 @@ class SarifReportTest { val report = SarifReport( testCases = testCases, generatedTestsCode = "", - SourceFindingStrategyDefault("", "", "", "") + sourceFindingEmpty ).createReport().toSarif() assert(report.runs.first().results[0].message.text.contains("NullPointerException")) @@ -74,23 +74,9 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) Mockito.`when`(mockUtExecution.path.lastOrNull()?.stmt?.javaSourceStartLineNumber).thenReturn(1337) - Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain") + Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") - val report = SarifReport( - testCases = listOf(testCase), - generatedTestsCode = """ - // comment for `startLine == 2` in related location - public void testMain() throws Throwable { - Main.main(); - } - """.trimIndent(), - SourceFindingStrategyDefault( - sourceClassFqn = "Main", - sourceFilePath = "src/Main.java", - testsFilePath = "test/MainTest.java", - projectRootPath = "." - ) - ).createReport().toSarif() + val report = sarifReportMain.createReport().toSarif() val result = report.runs.first().results.first() val location = result.locations.first().physicalLocation @@ -99,7 +85,7 @@ class SarifReportTest { assert(location.artifactLocation.uri.contains("Main.java")) assert(location.region.startLine == 1337) assert(relatedLocation.artifactLocation.uri.contains("MainTest.java")) - assert(relatedLocation.region.startLine == 2) + assert(relatedLocation.region.startLine == 1) } @Test @@ -117,16 +103,7 @@ class SarifReportTest { ) ) - val report = SarifReport( - testCases = listOf(testCase), - generatedTestsCode = "", - SourceFindingStrategyDefault( - sourceClassFqn = "Main", - sourceFilePath = "src/Main.java", - testsFilePath = "test/MainTest.java", - projectRootPath = "." - ) - ).createReport().toSarif() + val report = sarifReportMain.createReport().toSarif() val result = report.runs.first().results.first() assert(result.message.text.contains("227")) @@ -135,14 +112,13 @@ class SarifReportTest { } @Test - fun testCorrectCodeFlow() { + fun testCorrectCodeFlows() { mockUtMethodNames() val uncheckedException = Mockito.mock(NullPointerException::class.java) + val stackTraceElement = StackTraceElement("Main", "main", "Main.java", 17) Mockito.`when`(uncheckedException.stackTrace).thenReturn( - Array(2) { - StackTraceElement("Main", "main", "Main.java", 17) - } + Array(2) { stackTraceElement } ) Mockito.`when`(mockUtExecution.result).thenReturn( @@ -150,24 +126,61 @@ class SarifReportTest { ) Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) - val report = SarifReport( - testCases = listOf(testCase), - generatedTestsCode = "", - SourceFindingStrategyDefault( - sourceClassFqn = "Main", - sourceFilePath = "src/Main.java", - testsFilePath = "test/MainTest.java", - projectRootPath = "." - ) - ).createReport().toSarif() + val report = sarifReportMain.createReport().toSarif() val result = report.runs.first().results.first().codeFlows.first().threadFlows.first().locations.map { it.location.physicalLocation } - assert(result[0].artifactLocation.uri.contains("Main.java")) - assert(result[0].region.startLine == 17) - assert(result[1].artifactLocation.uri.contains("Main.java")) - assert(result[1].region.startLine == 17) + for (index in 0..1) { + assert(result[index].artifactLocation.uri.contains("Main.java")) + assert(result[index].region.startLine == 17) + } + } + + @Test + fun testCodeFlowsStartsWithMethodCall() { + mockUtMethodNames() + + val uncheckedException = Mockito.mock(NullPointerException::class.java) + val stackTraceElement = StackTraceElement("Main", "main", "Main.java", 3) + Mockito.`when`(uncheckedException.stackTrace).thenReturn(arrayOf(stackTraceElement)) + + Mockito.`when`(mockUtExecution.result).thenReturn( + UtImplicitlyThrownException(uncheckedException, false) + ) + Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) + Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") + + val report = sarifReportMain.createReport().toSarif() + + val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { + it.location.physicalLocation + } + assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java")) + assert(codeFlowPhysicalLocations[0].region.startLine == 3) + } + + @Test + fun testCodeFlowsStartsWithPrivateMethodCall() { + mockUtMethodNames() + + val uncheckedException = Mockito.mock(NullPointerException::class.java) + val stackTraceElement = StackTraceElement("Main", "main", "Main.java", 3) + Mockito.`when`(uncheckedException.stackTrace).thenReturn(arrayOf(stackTraceElement)) + + Mockito.`when`(mockUtExecution.result).thenReturn( + UtImplicitlyThrownException(uncheckedException, false) + ) + Mockito.`when`(mockUtExecution.stateBefore.parameters).thenReturn(listOf()) + Mockito.`when`(mockUtExecution.testMethodName).thenReturn("testMain_ThrowArithmeticException") + + val report = sarifReportPrivateMain.createReport().toSarif() + + val codeFlowPhysicalLocations = report.runs[0].results[0].codeFlows[0].threadFlows[0].locations.map { + it.location.physicalLocation + } + assert(codeFlowPhysicalLocations[0].artifactLocation.uri.contains("MainTest.java")) + assert(codeFlowPhysicalLocations[0].region.startLine == 4) } // internal @@ -184,4 +197,41 @@ class SarifReportTest { } private fun String.toSarif(): Sarif = jacksonObjectMapper().readValue(this) + + // constants + + private val sourceFindingEmpty = SourceFindingStrategyDefault( + sourceClassFqn = "", + sourceFilePath = "", + testsFilePath = "", + projectRootPath = "" + ) + + private val sourceFindingMain = SourceFindingStrategyDefault( + sourceClassFqn = "Main", + sourceFilePath = "src/Main.java", + testsFilePath = "test/MainTest.java", + projectRootPath = "." + ) + + private val generatedTestsCodeMain = """ + public void testMain_ThrowArithmeticException() { + Main main = new Main(); + main.main(0); + } + """.trimIndent() + + private val generatedTestsCodePrivateMain = """ + public void testMain_ThrowArithmeticException() { + Main main = new Main(); + // ... + mainMethod.invoke(main, mainMethodArguments); + } + """.trimIndent() + + private val sarifReportMain = + SarifReport(listOf(testCase), generatedTestsCodeMain, sourceFindingMain) + + private val sarifReportPrivateMain = + SarifReport(listOf(testCase), generatedTestsCodePrivateMain, sourceFindingMain) } \ No newline at end of file diff --git a/utbot-fuzzers/build.gradle b/utbot-fuzzers/build.gradle index 1f0870c630..fd94590950 100644 --- a/utbot-fuzzers/build.gradle +++ b/utbot-fuzzers/build.gradle @@ -1,3 +1,7 @@ +plugins { + id "com.github.johnrengelman.shadow" version "6.1.0" +} + apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" dependencies { @@ -9,3 +13,9 @@ dependencies { compileJava { options.compilerArgs = [] } + +shadowJar { + configurations = [project.configurations.compileClasspath] + archiveClassifier.set('') + minimize() +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt index 83f1edbb23..ec26297d01 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/CartesianProduct.kt @@ -1,18 +1,27 @@ package org.utbot.fuzzer +import kotlin.random.Random + /** * Creates iterable for all values of cartesian product of `lists`. */ -class CartesianProduct(private val lists: List>): Iterable> { +class CartesianProduct( + private val lists: List>, + private val random: Random? = null +): Iterable> { fun asSequence(): Sequence> = iterator().asSequence() override fun iterator(): Iterator> { - return Combinations(*lists.map { it.size }.toIntArray()) - .asSequence() - .map { combination -> - combination.mapIndexedTo(mutableListOf()) { element, value -> lists[element][value] } - } - .iterator() + val combinations = Combinations(*lists.map { it.size }.toIntArray()) + val sequence = if (random != null) { + val permutation = PseudoShuffledIntProgression(combinations.size, random) + (0 until combinations.size).asSequence().map { combinations[permutation[it]] } + } else { + combinations.asSequence() + } + return sequence.map { combination -> + combination.mapIndexedTo(mutableListOf()) { element, value -> lists[element][value] } + }.iterator() } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ConstantsModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ConstantsModelProvider.kt deleted file mode 100644 index 5b0106a748..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ConstantsModelProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.isPrimitive -import org.utbot.framework.plugin.api.util.stringClassId -import java.util.function.BiConsumer - -/** - * Traverses through method constants and creates appropriate models for them. - */ -object ConstantsModelProvider : ModelProvider { - - override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { - description.concreteValues - .asSequence() - .filter { (classId, _) -> classId.isPrimitive || classId == stringClassId } - .forEach { (_, value) -> - val model = UtPrimitiveModel(value) - description.parametersMap.getOrElse(model.classId) { emptyList() }.forEach { index -> - consumer.accept(index, model) - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/DefaultModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/DefaultModelProvider.kt deleted file mode 100644 index 625d1dfacb..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/DefaultModelProvider.kt +++ /dev/null @@ -1,26 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.* -import java.util.function.BiConsumer -import java.util.function.Function - -/** - * Simple model implementation. - * - * @param classToModel creates a list of [UtModel] for a particular class. - */ -@Suppress("unused") -class DefaultModelProvider( - private val classToModel: Function> -) : ModelProvider { - override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { - description.parametersMap.forEach { (classId, indices) -> - val defaultModels = classToModel.apply(classId) - defaultModels.forEach { defaultModel -> - indices.forEach { index -> - consumer.accept(index, defaultModel) - } - } - } - } -} \ No newline at end of file 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 c7f106b4f6..86e706e804 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedMethodDescription.kt @@ -17,9 +17,19 @@ class FuzzedMethodDescription( val name: String, val returnType: ClassId, val parameters: List, - val concreteValues: List = emptyList() + val concreteValues: Collection = emptyList() ) { + /** + * Name that can be used to generate test names + */ + var compilableName: String? = null + + /** + * Returns parameter name by its index in the signature + */ + var parameterNameMap: (Int) -> String? = { null } + /** * Map class id to indices of this class in parameters list. */ @@ -31,8 +41,8 @@ class FuzzedMethodDescription( result } - constructor(executableId: ExecutableId, concreteValues: List = emptyList()) : this( - executableId.name, + constructor(executableId: ExecutableId, concreteValues: Collection = emptyList()) : this( + executableId.classId.simpleName + "." + executableId.name, executableId.returnType, executableId.parameters, concreteValues @@ -44,5 +54,33 @@ class FuzzedMethodDescription( */ data class FuzzedConcreteValue( val classId: ClassId, - val value: Any -) \ No newline at end of file + val value: Any, + val relativeOp: FuzzedOp = FuzzedOp.NONE, +) + +enum class FuzzedOp(val sign: String?) { + 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/FuzzedValue.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt new file mode 100644 index 0000000000..3919f111cd --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/FuzzedValue.kt @@ -0,0 +1,26 @@ +package org.utbot.fuzzer + +import org.utbot.framework.plugin.api.UtModel + +/** + * Fuzzed Value stores information about concrete UtModel, reference to [ModelProvider] + * and reasons about why this value was generated. + */ +class FuzzedValue( + val model: UtModel, + val createdBy: ModelProvider? = null +) { + + /** + * Summary is a piece of useful information that clarify why this value has a concrete value. + * + * It supports a special character `%var%` that is used as a placeholder for parameter name. + * + * For example: + * 1. `%var% = 2` for a value that have value 2 + * 2. `%var% >= 4` for a value that shouldn't be less than 4 + * 3. `foo(%var%) returns true` for values that should be passed as a function parameter + * 4. `%var% has special characters` to describe content + */ + var summary: String? = null +} \ 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 8878725bd4..b15bbf19d9 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/Fuzzer.kt @@ -1,16 +1,24 @@ package org.utbot.fuzzer -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.fuzzer.providers.ConstantsModelProvider +import org.utbot.fuzzer.providers.ObjectModelProvider +import org.utbot.fuzzer.providers.PrimitivesModelProvider +import org.utbot.fuzzer.providers.StringConstantModelProvider import mu.KotlinLogging +import org.utbot.fuzzer.providers.ArrayModelProvider +import org.utbot.fuzzer.providers.CharToStringModelProvider +import org.utbot.fuzzer.providers.CollectionModelProvider +import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider +import org.utbot.fuzzer.providers.EnumModelProvider +import org.utbot.fuzzer.providers.PrimitiveWrapperModelProvider import java.util.concurrent.atomic.AtomicInteger -import java.util.function.ToIntFunction +import java.util.function.IntSupplier +import kotlin.random.Random private val logger = KotlinLogging.logger {} -fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence> { - val values = List>(description.parameters.size) { mutableListOf() } +fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvider): Sequence> { + val values = List>(description.parameters.size) { mutableListOf() } modelProviders.forEach { fuzzingProvider -> fuzzingProvider.generate(description) { index, model -> values[index].add(model) @@ -19,20 +27,47 @@ fun fuzz(description: FuzzedMethodDescription, vararg modelProviders: ModelProvi description.parameters.forEachIndexed { index, classId -> val models = values[index] if (models.isEmpty()) { - logger.warn { "There's no models provided classId=$classId. Null or default value will be provided" } - models.add(classId.defaultValueModel()) + logger.warn { "There's no models provided classId=$classId. No suitable values are generated for ${description.name}" } + return emptySequence() } } - return CartesianProduct(values).asSequence() + return CartesianProduct(values, Random(0L)).asSequence() } -fun defaultModelProviders(idGenerator: ToIntFunction = SimpleIdGenerator()): ModelProvider { - return ObjectModelProvider(idGenerator) - .with(PrimitivesModelProvider) - .with(ConstantsModelProvider) +/** + * Creates a model provider from a list of default providers. + */ +fun defaultModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): ModelProvider { + return ModelProvider.of( + ObjectModelProvider(idGenerator), + CollectionModelProvider(idGenerator), + ArrayModelProvider(idGenerator), + EnumModelProvider, + ConstantsModelProvider, + StringConstantModelProvider, + CharToStringModelProvider, + PrimitivesModelProvider, + PrimitiveWrapperModelProvider, + ) } -private class SimpleIdGenerator : ToIntFunction { +/** + * Creates a model provider for [ObjectModelProvider] that generates values for object constructor. + */ +fun objectModelProviders(idGenerator: IntSupplier = SimpleIdGenerator()): ModelProvider { + return ModelProvider.of( + CollectionModelProvider(idGenerator), + ArrayModelProvider(idGenerator), + EnumModelProvider, + StringConstantModelProvider, + CharToStringModelProvider, + ConstantsModelProvider, + PrimitiveDefaultsModelProvider, + PrimitiveWrapperModelProvider, + ) +} + +private class SimpleIdGenerator : IntSupplier { private val id = AtomicInteger() - override fun applyAsInt(value: ClassId?) = id.incrementAndGet() + override fun getAsInt() = id.incrementAndGet() } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt index d7dec7d870..45d51fd83c 100644 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ModelProvider.kt @@ -12,31 +12,47 @@ fun interface ModelProvider { * @param description a fuzzed method description * @param consumer accepts index in the parameter list and [UtModel] for this parameter. */ - fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) + fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) /** * Combines this model provider with `anotherModelProvider` into one instance. * * This model provider is called before `anotherModelProvider`. */ - fun with(anotherModelProvider: ModelProvider) : ModelProvider { - return ModelProvider { description, consumer -> - this@ModelProvider.generate(description, consumer) - anotherModelProvider.generate(description, consumer) + fun with(anotherModelProvider: ModelProvider): ModelProvider { + fun toList(m: ModelProvider) = if (m is Combined) m.providers else listOf(m) + return Combined(toList(this) + toList(anotherModelProvider)) + } + + /** + * Removes `anotherModelProvider` from current one. + */ + fun except(anotherModelProvider: ModelProvider): ModelProvider { + return except { it == anotherModelProvider } + } + + /** + * Removes `anotherModelProvider` from current one. + */ + fun except(filter: (ModelProvider) -> Boolean): ModelProvider { + return if (this is Combined) { + Combined(providers.filterNot(filter)) + } else { + Combined(if (filter(this)) emptyList() else listOf(this)) } } /** - * Creates [ModelProvider] that passes unprocessed classes to `fallbackModelSupplier` function. + * Creates [ModelProvider] that passes unprocessed classes to `modelProvider`. * - * This model provider is called before function is called, therefore consumer will get values - * from this model provider and only after it created by `fallbackModelSupplier`. + * Returned model provider is called before `modelProvider` is called, therefore consumer will get values + * from returned model provider and only after it calls `modelProvider`. * - * @param fallbackModelSupplier is called for every [ClassId] which wasn't created by this model provider. + * @param modelProvider is called and every value of [ClassId] is collected which wasn't created by this model provider. */ - fun withFallback(fallbackModelSupplier: (ClassId) -> UtModel?) : ModelProvider { + fun withFallback(modelProvider: ModelProvider) : ModelProvider { return ModelProvider { description, consumer -> - val providedByDelegateMethodParameters = mutableMapOf>() + val providedByDelegateMethodParameters = mutableMapOf>() this@ModelProvider.generate(description) { index, model -> providedByDelegateMethodParameters.computeIfAbsent(index) { mutableListOf() }.add(model) } @@ -45,13 +61,75 @@ fun interface ModelProvider { consumer.accept(index, model) } } - (0 until description.parameters.size) - .filter { !providedByDelegateMethodParameters.containsKey(it) } - .associateWith { description.parameters[it] } - .forEach { (index, classId) -> - fallbackModelSupplier(classId)?.let { consumer.accept(index, it) } + val missingParameters = + (0 until description.parameters.size).filter { !providedByDelegateMethodParameters.containsKey(it) } + if (missingParameters.isNotEmpty()) { + val values = mutableMapOf>() + modelProvider.generate(description) { i, m -> values.computeIfAbsent(i) { mutableListOf() }.add(m) } + missingParameters.forEach { index -> + values[index]?.let { models -> + models.forEach { model -> + consumer.accept(index, model) + } + } } + } } } + /** + * Creates [ModelProvider] that passes unprocessed classes to `fallbackModelSupplier` function. + * + * This model provider is called before function is called, therefore consumer will get values + * from this model provider and only after it created by `fallbackModelSupplier`. + * + * @param fallbackModelSupplier is called for every [ClassId] which wasn't created by this model provider. + */ + fun withFallback(fallbackModelSupplier: (ClassId) -> UtModel?) : ModelProvider { + return withFallback { description, consumer -> + description.parametersMap.forEach { (classId, indices) -> + fallbackModelSupplier(classId)?.let { model -> + indices.forEach { index -> + consumer.accept(index, model.fuzzed()) + } + } + } + } + } + + companion object { + @JvmStatic + fun of(vararg providers: ModelProvider): ModelProvider { + return Combined(providers.toList()) + } + + fun BiConsumer.consumeAll(indices: List, models: Sequence) { + models.forEach { model -> + indices.forEach { index -> + accept(index, model) + } + } + } + + fun BiConsumer.consumeAll(indices: List, models: List) { + consumeAll(indices, models.asSequence()) + } + } + + /** + * Wrapper class that delegates implementation to the [providers]. + */ + private class Combined(val providers: List): ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + providers.forEach { provider -> + provider.generate(description, consumer) + } + } + } + + fun UtModel.fuzzed(block: FuzzedValue.() -> Unit = {}): FuzzedValue = FuzzedValue(this, this@ModelProvider).apply(block) +} + +inline fun ModelProvider.exceptIsInstance(): ModelProvider { + return except { it is T } } \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/NullModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/NullModelProvider.kt deleted file mode 100644 index 18d0135ad6..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/NullModelProvider.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtNullModel -import org.utbot.framework.plugin.api.util.isRefType -import java.util.function.BiConsumer - -/** - * Provides [UtNullModel] for every reference class. - */ -@Suppress("unused") // disabled until fuzzer breaks test with null/nonnull annotations -object NullModelProvider : ModelProvider { - override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { - description.parameters - .asSequence() - .filter { classId -> classId.isRefType } - .forEachIndexed { index, classId -> - consumer.accept(index, UtNullModel(classId)) - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ObjectModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ObjectModelProvider.kt deleted file mode 100644 index 5952091a25..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/ObjectModelProvider.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.ClassId -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.UtModel -import org.utbot.framework.plugin.api.UtStatementModel -import org.utbot.framework.plugin.api.util.id -import org.utbot.framework.plugin.api.util.jClass -import java.lang.reflect.Constructor -import java.lang.reflect.Modifier -import java.lang.reflect.Parameter -import java.util.function.BiConsumer -import java.util.function.ToIntFunction - -/** - * Creates [UtAssembleModel] for objects which have public constructors with primitives types and String as parameters. - */ -class ObjectModelProvider( - private val idGenerator: ToIntFunction -) : ModelProvider { - - override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { - val assembleModels = with(description) { - parameters.asSequence() - .flatMap { classId -> - collectConstructors(classId) { javaConstructor -> - isPublic(javaConstructor) && javaConstructor.parameters.all(::isPrimitiveOrString) - } - } - .associateWith { - fuzzParameters(it) - } - .flatMap { (constructorId, fuzzedParameters) -> - fuzzedParameters.map { params -> - assembleModel(idGenerator.applyAsInt(constructorId.classId), constructorId, params) - } - } - } - - assembleModels.forEach { assembleModel -> - description.parametersMap[assembleModel.classId]?.forEach { index -> - consumer.accept(index, assembleModel) - } - } - } - - companion object { - private fun collectConstructors(classId: ClassId, predicate: (Constructor<*>) -> Boolean): Sequence { - return classId.jClass.declaredConstructors.asSequence() - .filter(predicate) - .map { javaConstructor -> - ConstructorId(classId, javaConstructor.parameters.map { it.type.id }) - } - } - - private fun isPublic(javaConstructor: Constructor<*>): Boolean { - return javaConstructor.modifiers and Modifier.PUBLIC != 0 - } - - private fun isPrimitiveOrString(parameter: Parameter): Boolean { - val parameterType = parameter.type - return parameterType.isPrimitive || String::class.java == parameterType - } - - private fun FuzzedMethodDescription.fuzzParameters(constructorId: ConstructorId): Sequence> { - val fuzzedMethod = FuzzedMethodDescription( - executableId = constructorId, - concreteValues = this.concreteValues - ) - val modelProviders = arrayOf( - PrimitivesModelProvider, - ConstantsModelProvider - ) - return fuzz(fuzzedMethod, *modelProviders) - } - - private fun assembleModel(id: Int, constructorId: ConstructorId, params: List): UtAssembleModel { - val instantiationChain = mutableListOf() - return UtAssembleModel( - id, - constructorId.classId, - "${constructorId.classId.name}${constructorId.parameters}#" + id.toString(16), - instantiationChain - ).apply { - instantiationChain += UtExecutableCallModel(null, constructorId, params, this) - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PrimitivesModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PrimitivesModelProvider.kt deleted file mode 100644 index 5098b7ff83..0000000000 --- a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PrimitivesModelProvider.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.utbot.fuzzer - -import org.utbot.framework.plugin.api.UtModel -import org.utbot.framework.plugin.api.UtPrimitiveModel -import org.utbot.framework.plugin.api.util.* -import java.util.function.BiConsumer - -/** - * Creates models of primitives from the method parameters list. - */ -object PrimitivesModelProvider : ModelProvider { - override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { - description.parametersMap.forEach { (classId, parameterIndices) -> - val primitives = when (classId) { - byteClassId -> listOf( - UtPrimitiveModel(1.toByte()), - UtPrimitiveModel((-1).toByte()), - UtPrimitiveModel(Byte.MAX_VALUE), - UtPrimitiveModel(Byte.MIN_VALUE), - UtPrimitiveModel(0.toByte()), - ) - booleanClassId -> listOf(UtPrimitiveModel(false), UtPrimitiveModel(true)) - charClassId -> listOf( - UtPrimitiveModel('\\'), - UtPrimitiveModel('\t'), - UtPrimitiveModel('\n'), - UtPrimitiveModel('\u0000'), - ) - shortClassId -> listOf( - UtPrimitiveModel(1.toShort()), - UtPrimitiveModel((-1).toShort()), - UtPrimitiveModel(Short.MIN_VALUE), - UtPrimitiveModel(Short.MAX_VALUE), - UtPrimitiveModel(0.toShort()), - ) - intClassId -> listOf( - UtPrimitiveModel(1), - UtPrimitiveModel((-1)), - UtPrimitiveModel(Int.MIN_VALUE), - UtPrimitiveModel(Int.MAX_VALUE), - UtPrimitiveModel(0), - ) - longClassId -> listOf( - UtPrimitiveModel(1L), - UtPrimitiveModel(-1L), - UtPrimitiveModel(Long.MIN_VALUE), - UtPrimitiveModel(Long.MAX_VALUE), - UtPrimitiveModel(0L), - ) - floatClassId -> listOf( - UtPrimitiveModel(1.1f), - UtPrimitiveModel(-1.1f), - UtPrimitiveModel(Float.POSITIVE_INFINITY), - UtPrimitiveModel(Float.NEGATIVE_INFINITY), - UtPrimitiveModel(Float.MIN_VALUE), - UtPrimitiveModel(Float.MAX_VALUE), - UtPrimitiveModel(Float.NaN), - UtPrimitiveModel(0.0f), - ) - doubleClassId -> listOf( - UtPrimitiveModel(1.1), - UtPrimitiveModel(-1.1), - UtPrimitiveModel(Double.POSITIVE_INFINITY), - UtPrimitiveModel(Double.NEGATIVE_INFINITY), - UtPrimitiveModel(Double.MIN_VALUE), - UtPrimitiveModel(Double.MAX_VALUE), - UtPrimitiveModel(Double.NaN), - UtPrimitiveModel(0.0), - ) - stringClassId -> listOf( - UtPrimitiveModel(""), - UtPrimitiveModel(" "), - UtPrimitiveModel("nonemptystring"), - UtPrimitiveModel("multiline\n\rstring"), - UtPrimitiveModel("1"), - UtPrimitiveModel("False"), - ) - else -> listOf() - } - - primitives.forEach { model -> - parameterIndices.forEach { index -> - consumer.accept(index, model) - } - } - } - } -} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt new file mode 100644 index 0000000000..4ed08dd7a4 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/PseudoShuffledIntProgression.kt @@ -0,0 +1,237 @@ +package org.utbot.fuzzer + +import kotlin.math.sqrt +import kotlin.random.Random + +/** + * Generates pseudo random values from 0 to size exclusive. + * + * In general there are 2 ways to get random order for a given range: + * 1. Create an array of target size and shuffle values. + * 2. Create a set of generated values and generate new values until they're unique. + * + * Both cases cause high memory usage when target number of values is big. + * + * The memory usage can be reduced by using a pseudo-random sequence. + * + * Algorithm to create pseudo-random sequence of length L: + * 1. Take first K elements to create a matrix of size COLS Γ— ROWS in such way that K = COLS * ROWS and ROWS >= L - K. + * 2. Move last L - K elements into a new array tail. + * 3. Any index N from [0, K) can be calculated by using 2 numbers: number of column (i) and number of row (j) as follows: + * `N = i * ROWS + j` where `i = N % COLS` and `j = N / COLS`. + * In such case the index N increases from top to bottom and left to right. + * 4. Tail contains all values from [K, L). + * 5. Shuffle matrix columns, matrix rows and the tail. + * 6. Add the tail as a column to the matrix. Since ROWS >= L - K there are missing values for i = COLS and j >= L - K. + * 7. Write down all values except missing from left to right and top to bottom. + * + * Example, input size = 23 + * ``` + * matrix tail + * ----------------------- + * 0 5 10 15 20 + * 1 6 11 16 21 + * 2 7 12 17 22 + * 3 8 13 18 + * 4 9 14 19 + * ``` + * Columns are shuffled: + * + * ``` + * matrix tail + * ----------------------- + * 10 5 15 0 20 + * 11 6 16 1 21 + * 12 7 17 2 22 + * 13 8 18 3 + * 14 9 19 4 + * ``` + * Rows and tail are shuffled + * ``` + * matrix tail + * ----------------------- + * 12 7 17 2 22 + * 13 8 18 3 20 + * 14 9 19 4 21 + * 10 5 15 0 + * 11 6 16 1 + * ``` + * Merge matrix and tail: + * ``` + * 12 7 17 2 22 + * 13 8 18 3 20 + * 14 9 19 4 21 + * 10 5 15 0 Γ— + * 11 6 16 1 Γ— + * ``` + * + * Write down the sequence: `[12, 7, 17, 2, 22, 13, 8, 18, 3, 20, 14, 9, 19, 4, 21, 10, 5, 15, 0, 11, 6, 16, 1]`. + * + * Instead of storing matrix itself only column and row numbers can be stored. Therefore, the 5th step of the algorithm + * can be changed into this: + * + * 5. Shuffle column numbers, row numbers and tail. + * + * In this case any value from the matrix can be calculated as follows: + * `N = column[i] * ROWS + rows[j]`, where column and rows are shuffled column and row numbers arrays. + * + * Using number arrays instead of the matrix this algorithm requires only + * `537 552 bytes (~ 550 KB)` compared to `Int.MAX_VALUE * 4 = 8 589 934 588 bytes (~ 8 GB)` + * when using simple array-shuffle algorithm. + */ +class PseudoShuffledIntProgression : Iterable { + private val columnNumber: IntArray + private val rowNumber: IntArray + private val tail: IntArray + + val size: Int + + constructor(size: Int, random: Random = Random) : this(size, random, { sqrt(it.toDouble()).toInt() }) + + /** + * Test only constructor + */ + internal constructor(size: Int, random: Random, columns: (Int) -> Int) { + check(size >= 0) { "Size must be positive or 0 but current value is $size" } + this.size = size + var cols = columns(size) + if (cols > 0 && cols > size / cols) { + cols = size / cols + } + check(cols > 0 || size == 0) { "Side of matrix must be greater than 0 but $cols <= 0" } + + columnNumber = IntArray(size = cols) { it }.apply { shuffle(random) } + rowNumber = IntArray(size = if (cols == 0) 0 else size / cols) { it }.apply { shuffle(random) } + check(columnNumber.size <= rowNumber.size) { "Error in internal array state: number of rows shouldn't be less than number of columns" } + + val rectangle = columnNumber.size * rowNumber.size + tail = IntArray(size - rectangle) { it + rectangle }.apply { shuffle(random) } + } + + /** + * Test only constructor + */ + internal constructor(columns: IntArray, rows: IntArray, tail: IntArray) { + check(rows.size >= tail.size) { "Tail cannot be placed into 1 column of the target matrix" } + this.columnNumber = columns + this.rowNumber = rows + this.tail = tail + this.size = columns.size * rows.size + tail.size + } + + /** + * Returns a unique pseudo-random index for the current one. + * + * To calculate correct value of the merged matrix as described before + * let's look at not shuffled merged matrix with size 2 Γ— 6 and the tail with 2 elements. + * + * ``` + * 0 6 13 + * 1 7 14 + * 2 9 Γ— + * 3 10 Γ— + * 4 11 Γ— + * 5 12 Γ— + * ``` + * + * The index moves from left to right and top to bottom, so the result sequence should be + * `[0, 6, 13, 1, 7, 14, 2, 9, 3, 10, 4, 11, 5, 12]` + * but the correct index in this matrix cannot be calculated just as `i * ROWS + j` because of missing values. + * To correct the index a property shift should be used. Let's transform the matrix into a matrix of shift + * that should be added to the index for jumping over missing values: + * + * ``` + * 0 0 0 + * 0 0 0 + * 0 0 1 + * 1 2 2 + * 3 3 Γ— + * Γ— Γ— Γ— + * ``` + * + * The matrix can be divided into 3 parts that calculates correct indices: + * + * ``` + * 0 0 | 0 + * 0 0 | 0 + * _____|___ + * 0 0 1 + * 1 2 2 + * 3 3 Γ— + * Γ— Γ— Γ— + * ``` + * + * 1. Top left doesn't change the index and values are taken from the matrix. + * 2. Top right doesn't change the index and values are taken from the tail. + * 3. Bottom uses shifts to change index and values are taken from the matrix. + * + * To clarify general rule for calculating a shift let's look at merged matrix + * where every row has COLS values and TAILS missing values like this: + * ``` + * 1: N1 N2 N3 .. N_COLS | M1 M2 .. M_TAILS + * 2: N1 N2 N3 .. N_COLS | M1 M2 .. M_TAILS + * ... + * ROW: N1 N2 N3 .. N_COLS | M1 M2 .. M_TAILS + * ``` + * It can be shown that the shift changes every COLS values on TAILS, therefore the shift can be calculated as follows: + * ``` + * shift = (index / COLS) * TAILS + * ``` + * + * **Examples** + * + * COLS = 3, TAILS = 1: + * ``` + * 0 0 0 1 + * 1 1 2 2 + * 2 3 3 3 + * ... + * ``` + * + * COLS = 1, TAILS = 3 + * ``` + * 0 3 6 9 + * 12 15 18 21 + * 24 27 30 33 + * ... + * ``` + * + * COLS = 2, TAILS = 2 + * ``` + * 0 0 2 2 + * 4 4 6 6 + * 8 8 10 10 + * ... + * ``` + */ + operator fun get(index: Int): Int { + check(index in 0 until size) { "Index out of bounds: $index >= $size" } + val cols = columnNumber.size + val rows = rowNumber.size + val e = cols + 1 + var i = index % e + var j = index / e + return if (i == cols && j < tail.size) { + // top right case + tail[j] + } else { + // first tail.size * e values can be calculated without index shift + // o < 0 is the top left case + // o >= 0 is the bottom case with COLS = cols and TAILS = 1 + val o = ((index - tail.size * e) / cols).toLong() + if (o > 0) { + i = ((index + o) % e).toInt() + j = ((index + o) / e).toInt() + } + columnNumber[i] * rows + rowNumber[j] + } + } + + fun toArray(): IntArray = IntArray(size, this::get) + + override fun iterator(): IntIterator = object : IntIterator() { + var current = 0 + override fun hasNext() = current < size + override fun nextInt() = get(current++) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/MethodBasedNameSuggester.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/MethodBasedNameSuggester.kt new file mode 100644 index 0000000000..7dc6dbaa10 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/MethodBasedNameSuggester.kt @@ -0,0 +1,11 @@ +package org.utbot.fuzzer.names + +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue + +class MethodBasedNameSuggester : NameSuggester { + override fun suggest(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): Sequence { + return sequenceOf(TestSuggestedInfo("test${description.compilableName?.capitalize() ?: "Created"}ByFuzzer")) + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/ModelBasedNameSuggester.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/ModelBasedNameSuggester.kt new file mode 100644 index 0000000000..4c254a49d9 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/ModelBasedNameSuggester.kt @@ -0,0 +1,142 @@ +package org.utbot.fuzzer.names + +import org.utbot.framework.plugin.api.UtExecutionFailure +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.UtImplicitlyThrownException +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.exceptionOrNull +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue + +class ModelBasedNameSuggester( + private val suggester: List = listOf( + PrimitiveModelNameSuggester, + ArrayModelNameSuggester, + ) +): NameSuggester { + + var maxNumberOfParametersWhenNameIsSuggested = 3 + set(value) { + field = maxOf(0, value) + } + + override fun suggest(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): Sequence { + if (description.parameters.size > maxNumberOfParametersWhenNameIsSuggested) { + return emptySequence() + } + + return sequenceOf(TestSuggestedInfo( + testName = createTestName(description, values, result), + displayName = createDisplayName(description, values, result) + )) + } + + /** + * Name of a test. + * + * Result example: + * + * 1. *Without any information*: `testMethod` + * 2. *With parameters only*: `testMethodNameWithCornerCasesAndEmptyString` + * 3. *With return value*: `testMethodReturnZeroWithNonEmptyString` + * 4. *When throws an exception*: `testMethodThrowsNPEWithEmptyString` + */ + private fun createTestName(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): String { + val returnString = when (result) { + is UtExecutionSuccess -> (result.model as? UtPrimitiveModel)?.value?.let { v -> + when (v) { + is Number -> prettifyNumber(v) + is Boolean -> v.toString().capitalize() + else -> null + }?.let { "Returns$it" } + } + is UtExplicitlyThrownException, is UtImplicitlyThrownException -> result.exceptionOrNull()?.let { t -> + prettifyException(t).let { "Throws$it" } + } + else -> null + } ?: "" + + val parameters = values.asSequence() + .flatMap { value -> + suggester.map { suggester -> + suggester.suggest(description, value) + } + } + .filterNot { it.isNullOrBlank() } + .groupingBy { it } + .eachCount() + .entries + .joinToString(separator = "And") { (name, count) -> + name + if (count > 1) "s" else "" + } + + return buildString { + append("test") + append(description.compilableName?.capitalize() ?: "Method") + append(returnString) + if (parameters.isNotEmpty()) { + append("With", parameters) + } + } + } + + /** + * Display name of a test. + * + * Result example: + * 1. **Full name**: `firstArg = 12, secondArg < 100.0, thirdArg = empty string -> throw IllegalArgumentException` + * 2. **Name without appropriate information**: `arg_0 = 0 and others -> return 0` + */ + private fun createDisplayName(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): String { + val summaries = values.asSequence() + .mapIndexed { index, value -> + value.summary?.replace("%var%", description.parameterNameMap(index) ?: "arg_$index") + } + .filterNotNull() + .toList() + + val parameters = summaries.joinToString(postfix = if (summaries.size < values.size) " and others" else "") + + val returnValue = when(result) { + is UtExecutionSuccess -> result.model.let { m -> + when { + m is UtPrimitiveModel && m.classId != voidClassId -> "-> return " + m.value + m is UtNullModel -> "-> return null" + else -> null + } + } + is UtExplicitlyThrownException, is UtImplicitlyThrownException -> "-> throw ${(result as UtExecutionFailure).exception::class.java.simpleName}" + else -> null + } + + return listOfNotNull(parameters, returnValue).joinToString(separator = " ") + } + + companion object { + private fun prettifyNumber(value: T): String? { + return when { + value.toDouble() == 0.0 -> "Zero" + value.toDouble() == 1.0 -> "One" + value is Double -> when { + value.isNaN() -> "Nan" + value.isInfinite() -> "Infinity" + else -> null + } + (value is Byte || value is Short || value is Int || value is Long) && value.toLong() in 0..99999 -> value.toString() + else -> null + } + } + + private fun prettifyException(throwable: Throwable): String = + throwable.javaClass.simpleName + .toCharArray() + .asSequence() + .filter { it.isUpperCase() } + .joinToString(separator = "") + } + +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/NameSuggester.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/NameSuggester.kt new file mode 100644 index 0000000000..33300f7c31 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/NameSuggester.kt @@ -0,0 +1,16 @@ +package org.utbot.fuzzer.names + +import org.utbot.framework.plugin.api.UtExecutionResult +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue + +/** + * Name suggester generates a sequence of suggested test information such as: + * - test name + * - display test name. + */ +interface NameSuggester { + + fun suggest(description: FuzzedMethodDescription, values: List, result: UtExecutionResult?): Sequence + +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/SingleModelNameSuggester.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/SingleModelNameSuggester.kt new file mode 100644 index 0000000000..11339a87d4 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/SingleModelNameSuggester.kt @@ -0,0 +1,52 @@ +package org.utbot.fuzzer.names + +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue + +interface SingleModelNameSuggester { + fun suggest(description: FuzzedMethodDescription, value: FuzzedValue): String? +} + +object PrimitiveModelNameSuggester : SingleModelNameSuggester { + override fun suggest(description: FuzzedMethodDescription, value: FuzzedValue): String? { + val model = value.model + if (model is UtPrimitiveModel) { + val v = model.value + return when { + v == "" -> "EmptyString" + v is String && v.isBlank() -> "BlankString" + v is String && v.isNotEmpty() -> "NonEmptyString" + v is Char && (v == Char.MIN_VALUE || v == Char.MAX_VALUE) -> "CornerCase" + v is Byte && (v == Byte.MIN_VALUE || v == Byte.MAX_VALUE) -> "CornerCase" + v is Short && (v == Short.MIN_VALUE || v == Short.MAX_VALUE) -> "CornerCase" + v is Int && (v == Int.MIN_VALUE || v == Int.MAX_VALUE) -> "CornerCase" + v is Long && (v == Long.MIN_VALUE || v == Long.MAX_VALUE) -> "CornerCase" + v is Float && v.isInfinite() || v is Double && v.isInfinite() -> "CornerCase" + v is Float && v.isNaN() || v is Double && v.isNaN() -> "CornerCase" + v is Number && v.toDouble() == 0.0 -> "CornerCase" + else -> null + } + } + return null + } +} + +object ArrayModelNameSuggester : SingleModelNameSuggester { + override fun suggest(description: FuzzedMethodDescription, value: FuzzedValue): String? { + val model = value.model + if (model is UtArrayModel) { + return buildString { + if (model.length > 0) { + append("Non") + } + append("Empty") + append(if (model.classId.elementClassId?.isPrimitive == true ) "Primitive" else "Object") + append("Array") + } + } + return null + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/TestSuggestedInfo.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/TestSuggestedInfo.kt new file mode 100644 index 0000000000..45f212a321 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/names/TestSuggestedInfo.kt @@ -0,0 +1,9 @@ +package org.utbot.fuzzer.names + +/** + * Information that can be used to generate test names. + */ +class TestSuggestedInfo( + val testName: String, + val displayName: String? = null +) \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/AbstractModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/AbstractModelProvider.kt new file mode 100644 index 0000000000..d7dccf977c --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/AbstractModelProvider.kt @@ -0,0 +1,25 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.* +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Simple model implementation. + */ +@Suppress("unused") +abstract class AbstractModelProvider: ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap.forEach { (classId, indices) -> + toModel(classId)?.let { defaultModel -> + indices.forEach { index -> + consumer.accept(index, defaultModel.fuzzed()) + } + } + } + } + + abstract fun toModel(classId: ClassId): UtModel? +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt new file mode 100644 index 0000000000..92095009b2 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ArrayModelProvider.kt @@ -0,0 +1,34 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtArrayModel +import org.utbot.framework.plugin.api.util.defaultValueModel +import org.utbot.framework.plugin.api.util.isArray +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.consumeAll +import java.util.function.BiConsumer +import java.util.function.IntSupplier + +class ArrayModelProvider( + private val idGenerator: IntSupplier +) : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap + .asSequence() + .filter { (classId, _) -> classId.isArray } + .forEach { (arrayClassId, indices) -> + consumer.consumeAll(indices, listOf(0, 10).map { arraySize -> + UtArrayModel( + id = idGenerator.asInt, + arrayClassId, + length = arraySize, + arrayClassId.elementClassId!!.defaultValueModel(), + mutableMapOf() + ).fuzzed { + this.summary = "%var% = ${arrayClassId.elementClassId!!.simpleName}[$arraySize]" + } + }) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CharToStringModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CharToStringModelProvider.kt new file mode 100644 index 0000000000..16e20b6adc --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CharToStringModelProvider.kt @@ -0,0 +1,31 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.charClassId +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Collects all char constants and creates string with them. + */ +object CharToStringModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + val indices = description.parametersMap[stringClassId] ?: return + if (indices.isNotEmpty()) { + val string = description.concreteValues.asSequence() + .filter { it.classId == charClassId } + .map { it.value } + .filterIsInstance() + .joinToString(separator = "") + if (string.isNotEmpty()) { + val model = UtPrimitiveModel(string).fuzzed() + indices.forEach { + consumer.accept(it, model) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionModelProvider.kt new file mode 100644 index 0000000000..349ab90aef --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/CollectionModelProvider.kt @@ -0,0 +1,106 @@ +package org.utbot.fuzzer.providers + +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.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.util.id +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.consumeAll +import java.util.function.BiConsumer +import java.util.function.IntSupplier + +/** + * Provides different collection for concrete classes. + * + * For example, ArrayList, LinkedList, Collections.singletonList can be passed to check + * if that parameter breaks anything. For example, in case method doesn't expect + * a non-modifiable collection and tries to add values. + */ +class CollectionModelProvider( + private val idGenerator: IntSupplier +) : ModelProvider { + + private val generators = mapOf( + java.util.List::class.java to ::createListModels, + java.util.Set::class.java to ::createSetModels, + java.util.Map::class.java to ::createMapModels, + java.util.Collection::class.java to ::createCollectionModels, + java.lang.Iterable::class.java to ::createCollectionModels, + java.util.Iterator::class.java to ::createIteratorModels, + ) + + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap + .asSequence() + .forEach { (classId, indices) -> + generators[classId.jClass]?.let { createModels -> + consumer.consumeAll(indices, createModels().map { it.fuzzed() }) + } + } + } + + private fun createListModels(): List { + return listOf( + java.util.List::class.java.createdBy(java.util.ArrayList::class.java.asConstructor()), + java.util.List::class.java.createdBy(java.util.LinkedList::class.java.asConstructor()), + java.util.List::class.java.createdBy(java.util.Collections::class.java.methodCall("emptyList", java.util.List::class.java)), + java.util.List::class.java.createdBy(java.util.Collections::class.java.methodCall("synchronizedList", java.util.List::class.java, params = listOf(java.util.List::class.java)), listOf( + java.util.List::class.java.createdBy(java.util.ArrayList::class.java.asConstructor()) + )), + ) + } + + private fun createSetModels(): List { + return listOf( + java.util.Set::class.java.createdBy(java.util.HashSet::class.java.asConstructor()), + java.util.Set::class.java.createdBy(java.util.TreeSet::class.java.asConstructor()), + java.util.Set::class.java.createdBy(java.util.Collections::class.java.methodCall("emptySet", java.util.Set::class.java)) + ) + } + + private fun createMapModels(): List { + return listOf( + java.util.Map::class.java.createdBy(java.util.HashMap::class.java.asConstructor()), + java.util.Map::class.java.createdBy(java.util.TreeMap::class.java.asConstructor()), + java.util.Map::class.java.createdBy(java.util.Collections::class.java.methodCall("emptyMap", java.util.Map::class.java)), + ) + } + + private fun createCollectionModels(): List { + return listOf( + java.util.Collection::class.java.createdBy(java.util.ArrayList::class.java.asConstructor()), + java.util.Collection::class.java.createdBy(java.util.HashSet::class.java.asConstructor()), + java.util.Collection::class.java.createdBy(java.util.Collections::class.java.methodCall("emptySet", java.util.Set::class.java)), + ) + } + + private fun createIteratorModels(): List { + return listOf( + java.util.Iterator::class.java.createdBy(java.util.Collections::class.java.methodCall("emptyIterator", java.util.Iterator::class.java)), + ) + } + + private fun Class<*>.asConstructor() = ConstructorId(id, emptyList()) + + private fun Class<*>.methodCall(methodName: String, returnType: Class<*>, params: List> = emptyList()) = MethodId(id, methodName, returnType.id, params.map { it.id }) + + private fun Class<*>.createdBy(init: ExecutableId, params: List = emptyList()): UtAssembleModel { + val instantiationChain = mutableListOf() + val genId = idGenerator.asInt + return UtAssembleModel( + genId, + id, + "${init.classId.name}${init.parameters}#" + genId.toString(16), + instantiationChain + ).apply { + instantiationChain += UtExecutableCallModel(null, init, params, this) + } + } +} \ 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 new file mode 100644 index 0000000000..659a2281c2 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ConstantsModelProvider.kt @@ -0,0 +1,53 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Traverses through method constants and creates appropriate models for them. + */ +object ConstantsModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.concreteValues + .asSequence() + .filter { (classId, _) -> classId.isPrimitive } + .forEach { (_, value, op) -> + sequenceOf( + UtPrimitiveModel(value).fuzzed { summary = "%var% = $value" }, + modifyValue(value, op) + ) + .filterNotNull() + .forEach { m -> + description.parametersMap.getOrElse(m.model.classId) { emptyList() }.forEach { index -> + consumer.accept(index, m) + } + } + } + } + + 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 + 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-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/EnumModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/EnumModelProvider.kt new file mode 100644 index 0000000000..68af765931 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/EnumModelProvider.kt @@ -0,0 +1,24 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtEnumConstantModel +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.isSubtypeOf +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.consumeAll +import java.util.function.BiConsumer + +object EnumModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap + .asSequence() + .filter { (classId, _) -> classId.isSubtypeOf(Enum::class.java.id) } + .forEach { (classId, indices) -> + consumer.consumeAll(indices, classId.jClass.enumConstants.filterIsInstance>().map { + UtEnumConstantModel(classId, it).fuzzed { summary = "%var% = ${it.name}" } + }) + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NullModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NullModelProvider.kt new file mode 100644 index 0000000000..3aa9085d95 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/NullModelProvider.kt @@ -0,0 +1,24 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtNullModel +import org.utbot.framework.plugin.api.util.isRefType +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Provides [UtNullModel] for every reference class. + */ +@Suppress("unused") // disabled until fuzzer breaks test with null/nonnull annotations +object NullModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap + .asSequence() + .filter { (classId, _) -> classId.isRefType } + .forEach { (classId, indices) -> + val model = UtNullModel(classId) + indices.forEach { consumer.accept(it, model.fuzzed { this.summary = "%var% = null" }) } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt new file mode 100644 index 0000000000..52d9f0c054 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/ObjectModelProvider.kt @@ -0,0 +1,132 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.ClassId +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.util.id +import org.utbot.framework.plugin.api.util.isPrimitive +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.jClass +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.exceptIsInstance +import org.utbot.fuzzer.fuzz +import org.utbot.fuzzer.objectModelProviders +import org.utbot.fuzzer.providers.ConstantsModelProvider.fuzzed +import java.lang.reflect.Constructor +import java.lang.reflect.Modifier +import java.util.function.BiConsumer +import java.util.function.IntSupplier + +/** + * Creates [UtAssembleModel] for objects which have public constructors with primitives types and String as parameters. + */ +class ObjectModelProvider : ModelProvider { + + var modelProvider: ModelProvider + + private val idGenerator: IntSupplier + private val recursion: Int + private val limit: Int + + constructor(idGenerator: IntSupplier) : this(idGenerator, Int.MAX_VALUE) + + constructor(idGenerator: IntSupplier, limit: Int) : this(idGenerator, limit, 1) + + private constructor(idGenerator: IntSupplier, limit: Int, recursion: Int) { + this.idGenerator = idGenerator + this.recursion = recursion + this.limit = limit + this.modelProvider = objectModelProviders(idGenerator) + } + + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + val fuzzedValues = with(description) { + parameters.asSequence() + .filterNot { it == stringClassId || it.isPrimitiveWrapper } + .flatMap { classId -> + collectConstructors(classId) { javaConstructor -> + isPublic(javaConstructor) + }.sortedWith( + primitiveParameterizedConstructorsFirstAndThenByParameterCount + ).take(limit) + } + .associateWith { constructorId -> + val modelProviderWithoutRecursion = modelProvider.exceptIsInstance() + fuzzParameters( + constructorId, + if (recursion > 0) { + ObjectModelProvider(idGenerator, limit = 1, recursion - 1).with(modelProviderWithoutRecursion) + } else { + modelProviderWithoutRecursion.withFallback(NullModelProvider) + } + ) + } + .flatMap { (constructorId, fuzzedParameters) -> + if (constructorId.parameters.isEmpty()) { + sequenceOf(assembleModel(idGenerator.asInt, constructorId, emptyList())) + } + else { + fuzzedParameters.map { params -> + assembleModel(idGenerator.asInt, constructorId, params) + } + } + } + } + + fuzzedValues.forEach { fuzzedValue -> + description.parametersMap[fuzzedValue.model.classId]?.forEach { index -> + consumer.accept(index, fuzzedValue) + } + } + } + + companion object { + private fun collectConstructors(classId: ClassId, predicate: (Constructor<*>) -> Boolean): Sequence { + return classId.jClass.declaredConstructors.asSequence() + .filter(predicate) + .map { javaConstructor -> + ConstructorId(classId, javaConstructor.parameters.map { it.type.id }) + } + } + + private fun isPublic(javaConstructor: Constructor<*>): Boolean { + return javaConstructor.modifiers and Modifier.PUBLIC != 0 + } + + private fun FuzzedMethodDescription.fuzzParameters(constructorId: ConstructorId, vararg modelProviders: ModelProvider): Sequence> { + val fuzzedMethod = FuzzedMethodDescription( + executableId = constructorId, + concreteValues = this.concreteValues + ) + return fuzz(fuzzedMethod, *modelProviders) + } + + private fun assembleModel(id: Int, constructorId: ConstructorId, params: List): FuzzedValue { + val instantiationChain = mutableListOf() + return UtAssembleModel( + id, + constructorId.classId, + "${constructorId.classId.name}${constructorId.parameters}#" + id.toString(16), + instantiationChain + ).apply { + instantiationChain += UtExecutableCallModel(null, constructorId, params.map { it.model }, this) + }.fuzzed { + summary = "%var% = ${constructorId.classId.simpleName}(${constructorId.parameters.joinToString { it.simpleName }})" + } + } + + private val primitiveParameterizedConstructorsFirstAndThenByParameterCount = + compareByDescending { constructorId -> + constructorId.parameters.all { classId -> + classId.isPrimitive || classId == stringClassId + } + }.thenComparingInt { constructorId -> + constructorId.parameters.size + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveDefaultsModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveDefaultsModelProvider.kt new file mode 100644 index 0000000000..b28d0c69ee --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveDefaultsModelProvider.kt @@ -0,0 +1,45 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.charClassId +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 +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Provides default values for primitive types. + */ +object PrimitiveDefaultsModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap.forEach { (classId, parameterIndices) -> + valueOf(classId)?.let { model -> + parameterIndices.forEach { index -> + consumer.accept(index, model) + } + } + } + } + + fun valueOf(classId: ClassId): FuzzedValue? = when (classId) { + booleanClassId -> UtPrimitiveModel(false).fuzzed { summary = "%var% = false" } + byteClassId -> UtPrimitiveModel(0.toByte()).fuzzed { summary = "%var% = 0" } + charClassId -> UtPrimitiveModel('\u0000').fuzzed { summary = "%var% = \u0000" } + shortClassId -> UtPrimitiveModel(0.toShort()).fuzzed { summary = "%var% = 0" } + intClassId -> UtPrimitiveModel(0).fuzzed { summary = "%var% = 0" } + longClassId -> UtPrimitiveModel(0L).fuzzed { summary = "%var% = 0L" } + floatClassId -> UtPrimitiveModel(0.0f).fuzzed { summary = "%var% = 0f" } + doubleClassId -> UtPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" } + stringClassId -> UtPrimitiveModel("").fuzzed { summary = "%var% = \"\"" } + else -> null + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveWrapperModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveWrapperModelProvider.kt new file mode 100644 index 0000000000..2a568f9e04 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitiveWrapperModelProvider.kt @@ -0,0 +1,64 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.util.isPrimitiveWrapper +import org.utbot.framework.plugin.api.util.primitiveByWrapper +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.plugin.api.util.wrapperByPrimitive +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.ModelProvider.Companion.consumeAll +import java.util.function.BiConsumer + +object PrimitiveWrapperModelProvider: ModelProvider { + + private val constantModels = ModelProvider.of( + PrimitiveDefaultsModelProvider, + ConstantsModelProvider, + StringConstantModelProvider + ) + + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + val primitiveWrapperTypesAsPrimitiveTypes = description.parametersMap + .keys + .asSequence() + .filter { + it == stringClassId || it.isPrimitiveWrapper + } + .mapNotNull { classId -> + when { + classId == stringClassId -> stringClassId + classId.isPrimitiveWrapper -> primitiveByWrapper[classId] + else -> null + } + }.toList() + + if (primitiveWrapperTypesAsPrimitiveTypes.isEmpty()) { + return + } + + val constants = mutableMapOf>() + constantModels.generate(FuzzedMethodDescription( + name = this::class.simpleName + " constant generation ", + returnType = voidClassId, + parameters = primitiveWrapperTypesAsPrimitiveTypes, + concreteValues = description.concreteValues + )) { index, value -> + val primitiveWrapper = wrapperByPrimitive[primitiveWrapperTypesAsPrimitiveTypes[index]] + if (primitiveWrapper != null) { + constants.computeIfAbsent(primitiveWrapper) { mutableListOf() }.add(value) + } + } + + description.parametersMap + .asSequence() + .filter { (classId, _) -> classId == stringClassId || classId.isPrimitiveWrapper } + .forEach { (classId, indices) -> + constants[classId]?.let { models -> + consumer.consumeAll(indices, models) + } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitivesModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitivesModelProvider.kt new file mode 100644 index 0000000000..ee206cfbf8 --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/PrimitivesModelProvider.kt @@ -0,0 +1,89 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.* +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer + +/** + * Produces bound values for primitive types. + */ +object PrimitivesModelProvider : ModelProvider { + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + description.parametersMap.forEach { (classId, parameterIndices) -> + val primitives: List = when (classId) { + booleanClassId -> listOf( + UtPrimitiveModel(false).fuzzed { summary = "%var% = false" }, + UtPrimitiveModel(true).fuzzed { summary = "%var% = true" } + ) + charClassId -> listOf( + UtPrimitiveModel(Char.MIN_VALUE).fuzzed { summary = "%var% = Char.MIN_VALUE" }, + UtPrimitiveModel(Char.MAX_VALUE).fuzzed { summary = "%var% = Char.MAX_VALUE" }, + ) + byteClassId -> listOf( + UtPrimitiveModel(0.toByte()).fuzzed { summary = "%var% = 0" }, + UtPrimitiveModel(1.toByte()).fuzzed { summary = "%var% > 0" }, + UtPrimitiveModel((-1).toByte()).fuzzed { summary = "%var% < 0" }, + UtPrimitiveModel(Byte.MIN_VALUE).fuzzed { summary = "%var% = Byte.MIN_VALUE" }, + UtPrimitiveModel(Byte.MAX_VALUE).fuzzed { summary = "%var% = Byte.MAX_VALUE" }, + ) + shortClassId -> listOf( + UtPrimitiveModel(0.toShort()).fuzzed { summary = "%var% = 0" }, + UtPrimitiveModel(1.toShort()).fuzzed { summary = "%var% > 0" }, + UtPrimitiveModel((-1).toShort()).fuzzed { summary = "%var% < 0" }, + UtPrimitiveModel(Short.MIN_VALUE).fuzzed { summary = "%var% = Short.MIN_VALUE" }, + UtPrimitiveModel(Short.MAX_VALUE).fuzzed { summary = "%var% = Short.MAX_VALUE" }, + ) + intClassId -> listOf( + UtPrimitiveModel(0).fuzzed { summary = "%var% = 0" }, + UtPrimitiveModel(1).fuzzed { summary = "%var% > 0" }, + UtPrimitiveModel((-1)).fuzzed { summary = "%var% < 0" }, + UtPrimitiveModel(Int.MIN_VALUE).fuzzed { summary = "%var% = Int.MIN_VALUE" }, + UtPrimitiveModel(Int.MAX_VALUE).fuzzed { summary = "%var% = Int.MAX_VALUE" }, + ) + longClassId -> listOf( + UtPrimitiveModel(0L).fuzzed { summary = "%var% = 0L" }, + UtPrimitiveModel(1L).fuzzed { summary = "%var% > 0L" }, + UtPrimitiveModel(-1L).fuzzed { summary = "%var% < 0L" }, + UtPrimitiveModel(Long.MIN_VALUE).fuzzed { summary = "%var% = Long.MIN_VALUE" }, + UtPrimitiveModel(Long.MAX_VALUE).fuzzed { summary = "%var% = Long.MAX_VALUE" }, + ) + floatClassId -> listOf( + UtPrimitiveModel(0.0f).fuzzed { summary = "%var% = 0f" }, + UtPrimitiveModel(1.1f).fuzzed { summary = "%var% > 0f" }, + UtPrimitiveModel(-1.1f).fuzzed { summary = "%var% < 0f" }, + UtPrimitiveModel(Float.MIN_VALUE).fuzzed { summary = "%var% = Float.MIN_VALUE" }, + UtPrimitiveModel(Float.MAX_VALUE).fuzzed { summary = "%var% = Float.MAX_VALUE" }, + UtPrimitiveModel(Float.NEGATIVE_INFINITY).fuzzed { summary = "%var% = Float.NEGATIVE_INFINITY" }, + UtPrimitiveModel(Float.POSITIVE_INFINITY).fuzzed { summary = "%var% = Float.POSITIVE_INFINITY" }, + UtPrimitiveModel(Float.NaN).fuzzed { summary = "%var% = Float.NaN" }, + ) + doubleClassId -> listOf( + UtPrimitiveModel(0.0).fuzzed { summary = "%var% = 0.0" }, + UtPrimitiveModel(1.1).fuzzed { summary = "%var% > 0.0" }, + UtPrimitiveModel(-1.1).fuzzed { summary = "%var% < 0.0" }, + UtPrimitiveModel(Double.MIN_VALUE).fuzzed { summary = "%var% = Double.MIN_VALUE" }, + UtPrimitiveModel(Double.MAX_VALUE).fuzzed { summary = "%var% = Double.MAX_VALUE" }, + UtPrimitiveModel(Double.NEGATIVE_INFINITY).fuzzed { summary = "%var% = Double.NEGATIVE_INFINITY" }, + UtPrimitiveModel(Double.POSITIVE_INFINITY).fuzzed { summary = "%var% = Double.POSITIVE_INFINITY" }, + UtPrimitiveModel(Double.NaN).fuzzed { summary = "%var% = Double.NaN" }, + ) + stringClassId -> listOf( + UtPrimitiveModel("").fuzzed { summary = "%var% = empty string" }, + UtPrimitiveModel(" ").fuzzed { summary = "%var% = blank string" }, + UtPrimitiveModel("string").fuzzed { summary = "%var% != empty string" }, + UtPrimitiveModel("\n\t\r").fuzzed { summary = "%var% has special characters" }, + ) + else -> listOf() + } + + primitives.forEach { model -> + parameterIndices.forEach { index -> + consumer.accept(index, model) + } + } + } + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt new file mode 100644 index 0000000000..9beff9b93b --- /dev/null +++ b/utbot-fuzzers/src/main/kotlin/org/utbot/fuzzer/providers/StringConstantModelProvider.kt @@ -0,0 +1,48 @@ +package org.utbot.fuzzer.providers + +import org.utbot.framework.plugin.api.UtPrimitiveModel +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.ModelProvider +import java.util.function.BiConsumer +import kotlin.random.Random + +object StringConstantModelProvider : ModelProvider { + + override fun generate(description: FuzzedMethodDescription, consumer: BiConsumer) { + val random = Random(72923L) + description.concreteValues + .asSequence() + .filter { (classId, _) -> classId == stringClassId } + .forEach { (_, value, op) -> + listOf(value, mutate(random, value as? String, op)) + .asSequence() + .filterNotNull() + .map { UtPrimitiveModel(it) }.forEach { model -> + description.parametersMap.getOrElse(model.classId) { emptyList() }.forEach { index -> + consumer.accept(index, model.fuzzed { summary = "%var% = string" }) + } + } + } + } + + private fun mutate(random: Random, value: String?, op: FuzzedOp): String? { + if (value == null || value.isEmpty() || 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-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzedValueDescriptionTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzedValueDescriptionTest.kt new file mode 100644 index 0000000000..e08ae0f48f --- /dev/null +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/FuzzedValueDescriptionTest.kt @@ -0,0 +1,60 @@ +package org.utbot.framework.plugin.api + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.FuzzedValue +import org.utbot.fuzzer.providers.ConstantsModelProvider + +class FuzzedValueDescriptionTest { + + @Test + fun testConstantModelProviderTest() { + val values = mutableListOf() + val concreteValues = listOf( + FuzzedConcreteValue(intClassId, 10, FuzzedOp.EQ), + FuzzedConcreteValue(intClassId, 20, FuzzedOp.NE), + FuzzedConcreteValue(intClassId, 30, FuzzedOp.LT), + FuzzedConcreteValue(intClassId, 40, FuzzedOp.LE), + FuzzedConcreteValue(intClassId, 50, FuzzedOp.GT), + FuzzedConcreteValue(intClassId, 60, FuzzedOp.GE), + ) + val summaries = listOf( + "%var% = 10" to 10, + "%var% != 10" to 11, // 1, FuzzedOp.EQ -> %var% == 10: False + "%var% = 20" to 20, + "%var% != 20" to 21, // 2, FuzzedOp.NE -> %var% != 20: True + "%var% = 30" to 30, + "%var% < 30" to 29, // 3, FuzzedOp.LT -> %var% < 30: True + "%var% = 40" to 40, + "%var% > 40" to 41, // 4, FuzzedOp.LE -> %var% <= 40: False + "%var% = 50" to 50, + "%var% > 50" to 51, // 5, FuzzedOp.GT -> %var% > 50: True + "%var% = 60" to 60, + "%var% < 60" to 59, // 6, FuzzedOp.GE -> %var% >= 60: False + ) + val expected = concreteValues.size * 2 + ConstantsModelProvider.generate( + FuzzedMethodDescription( + name = "name", + returnType = voidClassId, + parameters = listOf(intClassId), + concreteValues = concreteValues + ) + ) { _, value -> values.add(value) } + assertEquals(expected, values.size) { + "Expected $expected values: a half is origin values and another is modified, but only ${values.size} are generated" + } + for (i in summaries.indices) { + assertEquals(summaries[i].second, (values[i].model as UtPrimitiveModel).value) { + "Constant model provider should change constant values to reverse if-statement" + } + assertEquals(summaries[i].first, values[i].summary) + } + } + +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt new file mode 100644 index 0000000000..106a4a3d62 --- /dev/null +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/ModelProviderTest.kt @@ -0,0 +1,451 @@ +package org.utbot.framework.plugin.api + +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.booleanClassId +import org.utbot.framework.plugin.api.util.byteClassId +import org.utbot.framework.plugin.api.util.charClassId +import org.utbot.framework.plugin.api.util.doubleClassId +import org.utbot.framework.plugin.api.util.floatClassId +import org.utbot.framework.plugin.api.util.id +import org.utbot.framework.plugin.api.util.intClassId +import org.utbot.framework.plugin.api.util.longClassId +import org.utbot.framework.plugin.api.util.shortClassId +import org.utbot.framework.plugin.api.util.stringClassId +import org.utbot.framework.plugin.api.util.voidClassId +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.fuzzer.FuzzedConcreteValue +import org.utbot.fuzzer.FuzzedMethodDescription +import org.utbot.fuzzer.FuzzedOp +import org.utbot.fuzzer.ModelProvider +import org.utbot.fuzzer.providers.ConstantsModelProvider +import org.utbot.fuzzer.providers.ObjectModelProvider +import org.utbot.fuzzer.providers.PrimitivesModelProvider +import org.utbot.fuzzer.providers.StringConstantModelProvider +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.utbot.framework.plugin.api.util.primitiveByWrapper +import org.utbot.framework.plugin.api.util.primitiveWrappers +import org.utbot.framework.plugin.api.util.voidWrapperClassId +import org.utbot.fuzzer.defaultModelProviders +import org.utbot.fuzzer.providers.EnumModelProvider +import org.utbot.fuzzer.providers.EnumModelProvider.fuzzed +import org.utbot.fuzzer.providers.PrimitiveDefaultsModelProvider +import java.util.Date + +class ModelProviderTest { + + @Test + fun `test generate primitive models for boolean`() { + val models = collect(PrimitivesModelProvider, + parameters = listOf(booleanClassId) + ) + + assertEquals(1, models.size) + assertEquals(2, models[0]!!.size) + assertTrue(models[0]!!.contains(UtPrimitiveModel(true))) + assertTrue(models[0]!!.contains(UtPrimitiveModel(false))) + } + + @Test + fun `test all known primitive types are generate at least one value`() { + val primitiveTypes = listOf( + byteClassId, + booleanClassId, + charClassId, + shortClassId, + intClassId, + longClassId, + floatClassId, + doubleClassId, + stringClassId, + ) + val models = collect(PrimitivesModelProvider, + parameters = primitiveTypes + ) + + assertEquals(primitiveTypes.size, models.size) + primitiveTypes.indices.forEach { + assertTrue(models[it]!!.isNotEmpty()) + } + } + + @Test + fun `test that empty constants don't generate any models`() { + val models = collect(ConstantsModelProvider, + parameters = listOf(intClassId), + constants = emptyList() + ) + + assertEquals(0, models.size) + } + + @Test + fun `test that one constant generate corresponding value`() { + val models = collect(ConstantsModelProvider, + parameters = listOf(intClassId), + constants = listOf( + FuzzedConcreteValue(intClassId, 123) + ) + ) + + assertEquals(1, models.size) + assertEquals(1, models[0]!!.size) + assertEquals(UtPrimitiveModel(123), models[0]!![0]) + assertEquals(intClassId, models[0]!![0].classId) + } + + @Test + fun `test that constants are mutated if comparison operation is set`() { + val models = collect(ConstantsModelProvider, + parameters = listOf(intClassId), + constants = listOf( + FuzzedConcreteValue(intClassId, 10, FuzzedOp.EQ), + FuzzedConcreteValue(intClassId, 20, FuzzedOp.NE), + FuzzedConcreteValue(intClassId, 30, FuzzedOp.LT), + FuzzedConcreteValue(intClassId, 40, FuzzedOp.LE), + FuzzedConcreteValue(intClassId, 50, FuzzedOp.GT), + FuzzedConcreteValue(intClassId, 60, FuzzedOp.GE), + ) + ) + + assertEquals(1, models.size) + val expectedValues = listOf(10, 11, 20, 21, 29, 30, 40, 41, 50, 51, 59, 60) + assertEquals(expectedValues.size, models[0]!!.size) + expectedValues.forEach { + assertTrue(models[0]!!.contains(UtPrimitiveModel(it))) + } + } + + @Test + fun `test constant empty string generates only corresponding model`() { + val models = collect(StringConstantModelProvider, + parameters = listOf(stringClassId), + constants = listOf( + FuzzedConcreteValue(stringClassId, "", FuzzedOp.CH), + ) + ) + + assertEquals(1, models.size) + assertEquals(1, models[0]!!.size) + assertEquals(UtPrimitiveModel(""), models[0]!![0]) + } + + @Test + fun `test non-empty string is not mutated if operation is not set`() { + val models = collect(StringConstantModelProvider, + parameters = listOf(stringClassId), + constants = listOf( + FuzzedConcreteValue(stringClassId, "nonemptystring", FuzzedOp.NONE), + ) + ) + + assertEquals(1, models.size) + assertEquals(1, models[0]!!.size) + assertEquals(UtPrimitiveModel("nonemptystring"), models[0]!![0]) + } + + @Test + fun `test non-empty string is mutated if modification operation is set`() { + val models = collect(StringConstantModelProvider, + parameters = listOf(stringClassId), + constants = listOf( + FuzzedConcreteValue(stringClassId, "nonemptystring", FuzzedOp.CH), + ) + ) + + assertEquals(1, models.size) + assertEquals(2, models[0]!!.size) + listOf("nonemptystring", "nonemptystr`ng").forEach { + assertTrue( models[0]!!.contains(UtPrimitiveModel(it))) { "Failed to find string $it in list ${models[0]}" } + } + } + + @Test + fun `test mutation creates the same values between different runs`() { + repeat(10) { + val models = collect(StringConstantModelProvider, + parameters = listOf(stringClassId), + constants = listOf( + FuzzedConcreteValue(stringClassId, "anotherstring", FuzzedOp.CH), + ) + ) + listOf("anotherstring", "anotherskring").forEach { + assertTrue( models[0]!!.contains(UtPrimitiveModel(it))) { "Failed to find string $it in list ${models[0]}" } + } + } + } + + @Test + @Suppress("unused", "UNUSED_PARAMETER", "RemoveEmptySecondaryConstructorBody") + fun `test default object model creation for simple constructors`() { + withUtContext(UtContext(this::class.java.classLoader)) { + class A { + constructor(a: Int) {} + constructor(a: Int, b: String) {} + constructor(a: Int, b: String, c: Boolean) + } + + val classId = A::class.java.id + val models = collect( + ObjectModelProvider { 0 }.apply { + modelProvider = ModelProvider.of(PrimitiveDefaultsModelProvider) + }, + parameters = listOf(classId) + ) + + assertEquals(1, models.size) + assertEquals(3, models[0]!!.size) + assertTrue(models[0]!!.all { it is UtAssembleModel && it.classId == classId }) + + models[0]!!.filterIsInstance().forEachIndexed { index, model -> + assertEquals(1, model.instantiationChain.size) + val stm = model.instantiationChain[0] + assertTrue(stm is UtExecutableCallModel) + stm as UtExecutableCallModel + val paramCountInConstructorAsTheyListed = index + 1 + assertEquals(paramCountInConstructorAsTheyListed, stm.params.size) + } + } + } + + @Test + fun `test no object model is created for empty constructor`() { + withUtContext(UtContext(this::class.java.classLoader)) { + class A + + val classId = A::class.java.id + val models = collect( + ObjectModelProvider { 0 }, + parameters = listOf(classId) + ) + + assertEquals(1, models.size) + assertEquals(1, models[0]!!.size) + } + } + + @Test + @Suppress("unused", "UNUSED_PARAMETER") + fun `test that constructors with not primitive parameters are ignored`() { + withUtContext(UtContext(this::class.java.classLoader)) { + class A { + constructor(a: Int, b: Int) + constructor(a: Int, b: Date) + } + + val classId = A::class.java.id + val models = collect( + ObjectModelProvider { 0 }, + parameters = listOf(classId) + ) + + assertEquals(1, models.size) + assertTrue(models[0]!!.isNotEmpty()) + val chain = (models[0]!![0] as UtAssembleModel).instantiationChain + assertEquals(1, chain.size) + assertTrue(chain[0] is UtExecutableCallModel) + (chain[0] as UtExecutableCallModel).params.forEach { + assertEquals(intClassId, it.classId) + } + } + } + + @Test + fun `test fallback model can create custom values for any parameter`() { + val firstParameterIsUserGenerated = ModelProvider { _, consumer -> + consumer.accept(0, UtPrimitiveModel(-123).fuzzed()) + }.withFallback(PrimitivesModelProvider) + + val result = collect( + firstParameterIsUserGenerated, + parameters = listOf(intClassId, intClassId) + ) + + assertEquals(2, result.size) + assertEquals(1, result[0]!!.size) + assertTrue(result[1]!!.size > 1) + assertEquals(UtPrimitiveModel(-123), result[0]!![0]) + } + + @Test + fun `test collection model can produce basic values with assembled model`() { + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect( + defaultModelProviders { 0 }, + parameters = listOf(java.util.List::class.java.id) + ) + + assertEquals(1, result.size) + } + } + + @Test + fun `test enum model provider`() { + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect(EnumModelProvider, parameters = listOf(OneTwoThree::class.java.id)) + assertEquals(1, result.size) + assertEquals(3, result[0]!!.size) + OneTwoThree.values().forEachIndexed { index: Int, value -> + assertEquals(UtEnumConstantModel(OneTwoThree::class.java.id, value), result[0]!![index]) + } + } + } + + @Test + fun `test string value generates only primitive models`() { + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect(defaultModelProviders { 0 }, parameters = listOf(stringClassId)) + assertEquals(1, result.size) + result[0]!!.forEach { + assertInstanceOf(UtPrimitiveModel::class.java, it) + assertEquals(stringClassId, it.classId) + } + } + } + + @Test + fun `test wrapper primitives generate only primitive models`() { + withUtContext(UtContext(this::class.java.classLoader)) { + primitiveWrappers.asSequence().filterNot { it == voidWrapperClassId }.forEach { classId -> + val result = collect(defaultModelProviders { 0 }, parameters = listOf(classId)) + assertEquals(1, result.size) + result[0]!!.forEach { + assertInstanceOf(UtPrimitiveModel::class.java, it) + val expectPrimitiveBecauseItShouldBeGeneratedByDefaultProviders = primitiveByWrapper[classId] + assertEquals(expectPrimitiveBecauseItShouldBeGeneratedByDefaultProviders, it.classId) + } + } + } + } + + @Test + fun `test at least one string is created if characters exist as constants`() { + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect( + defaultModelProviders { 0 }, + parameters = listOf(stringClassId), + constants = listOf( + FuzzedConcreteValue(charClassId, 'a'), + FuzzedConcreteValue(charClassId, 'b'), + FuzzedConcreteValue(charClassId, 'c'), + ) + ) + assertEquals(1, result.size) + assertTrue(result[0]!!.any { + it is UtPrimitiveModel && it.value == "abc" + }) + } + } + + @Test + @Suppress("unused", "UNUSED_PARAMETER", "ConvertSecondaryConstructorToPrimary") + fun `test complex object is constructed and it is not null`() { + class A { + constructor(some: Any) + } + + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect(ObjectModelProvider { 0 }, parameters = listOf(A::class.java.id)) + assertEquals(1, result.size) + assertEquals(1, result[0]!!.size) + assertInstanceOf(UtAssembleModel::class.java, result[0]!![0]) + assertEquals(A::class.java.id, result[0]!![0].classId) + (result[0]!![0] as UtAssembleModel).instantiationChain.forEach { + assertTrue(it is UtExecutableCallModel) + assertEquals(1, (it as UtExecutableCallModel).params.size) + val objectParamInConstructor = it.params[0] + assertInstanceOf(UtAssembleModel::class.java, objectParamInConstructor) + val innerAssembledModel = objectParamInConstructor as UtAssembleModel + assertEquals(Any::class.java.id, innerAssembledModel.classId) + assertEquals(1, innerAssembledModel.instantiationChain.size) + val objectCreation = innerAssembledModel.instantiationChain.first() as UtExecutableCallModel + assertEquals(0, objectCreation.params.size) + assertInstanceOf(ConstructorId::class.java, objectCreation.executable) + } + } + } + + @Test + @Suppress("unused", "UNUSED_PARAMETER", "ConvertSecondaryConstructorToPrimary") + fun `test recursive constructor calls and can pass null into inner if no other values exist`() { + class MyA { + constructor(some: MyA?) + } + + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect(ObjectModelProvider { 0 }, parameters = listOf(MyA::class.java.id)) + assertEquals(1, result.size) + assertEquals(1, result[0]!!.size) + val outerModel = result[0]!![0] as UtAssembleModel + outerModel.instantiationChain.forEach { + val constructorParameters = (it as UtExecutableCallModel).params + assertEquals(1, constructorParameters.size) + val innerModel = (constructorParameters[0] as UtAssembleModel) + assertEquals(MyA::class.java.id, innerModel.classId) + assertEquals(1, innerModel.instantiationChain.size) + val innerConstructorParameters = innerModel.instantiationChain[0] as UtExecutableCallModel + assertEquals(1, innerConstructorParameters.params.size) + assertInstanceOf(UtNullModel::class.java, innerConstructorParameters.params[0]) + } + } + } + + @Test + @Suppress("unused", "UNUSED_PARAMETER", "ConvertSecondaryConstructorToPrimary") + fun `test complex object is constructed with the simplest inner object constructor`() { + + class Inner { + constructor(some: Inner?) + + constructor(some: Inner?, other: Any) + + // this constructor should be chosen + constructor(int: Int, double: Double) + + constructor(other: Any, int: Int) + + constructor(some: Inner?, other: Double) + } + + class Outer { + constructor(inner: Inner) + } + + withUtContext(UtContext(this::class.java.classLoader)) { + val result = collect(ObjectModelProvider { 0 }, parameters = listOf(Outer::class.java.id)) + assertEquals(1, result.size) + assertEquals(1, result[0]!!.size) + val outerModel = result[0]!![0] as UtAssembleModel + outerModel.instantiationChain.forEach { + val constructorParameters = (it as UtExecutableCallModel).params + assertEquals(1, constructorParameters.size) + val innerModel = (constructorParameters[0] as UtAssembleModel) + assertEquals(Inner::class.java.id, innerModel.classId) + assertEquals(1, innerModel.instantiationChain.size) + val innerConstructorParameters = innerModel.instantiationChain[0] as UtExecutableCallModel + assertEquals(2, innerConstructorParameters.params.size) + assertTrue(innerConstructorParameters.params.all { param -> param is UtPrimitiveModel }) + assertEquals(intClassId, innerConstructorParameters.params[0].classId) + assertEquals(doubleClassId, innerConstructorParameters.params[1].classId) + } + } + } + + private fun collect( + modelProvider: ModelProvider, + name: String = "testMethod", + returnType: ClassId = voidClassId, + parameters: List, + constants: List = emptyList() + ): Map> { + return mutableMapOf>().apply { + modelProvider.generate(FuzzedMethodDescription(name, returnType, parameters, constants)) { i, m -> + computeIfAbsent(i) { mutableListOf() }.add(m.model) + } + } + } + + private enum class OneTwoThree { + ONE, TWO, THREE + } +} \ No newline at end of file diff --git a/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt new file mode 100644 index 0000000000..6f1f6fd64c --- /dev/null +++ b/utbot-fuzzers/src/test/kotlin/org/utbot/framework/plugin/api/PseudoShuffledIntProgressionTest.kt @@ -0,0 +1,229 @@ +package org.utbot.framework.plugin.api + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import org.utbot.fuzzer.PseudoShuffledIntProgression +import kotlin.random.Random + +class PseudoShuffledIntProgressionTest { + + @Test + fun testEmpty() { + val shuffle = PseudoShuffledIntProgression(0) + assertEquals(0, shuffle.size) + assertThrows(IllegalStateException::class.java) { + shuffle[1] + } + assertThrows(IllegalStateException::class.java) { + shuffle[-1] + } + } + + @Test + fun testBadSize() { + assertThrows(IllegalStateException::class.java) { + PseudoShuffledIntProgression(-1) + } + } + + @Test + fun testBadSideAndCorrectSize() { + assertThrows(IllegalStateException::class.java) { + PseudoShuffledIntProgression(100, Random) { -it } + } + } + + @Test + fun testSingleValue() { + val shuffle = PseudoShuffledIntProgression(1) + assertEquals(0, shuffle[0]) + } + + @Test + fun testSequent() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0), intArrayOf(0, 1, 2, 3), intArrayOf()) + assertEquals(4, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 1, 2, 3), result) + } + + @Test + fun testSquare2() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1), intArrayOf(0, 1), intArrayOf()) + assertEquals(4, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 2, 1, 3), result) + } + + @Test + fun testSquare3() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1, 2), intArrayOf(0, 1, 2), intArrayOf()) + assertEquals(9, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 3, 6, 1, 4, 7, 2, 5, 8), result) + } + + @Test + fun testSquare2WithTail1() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1), intArrayOf(0, 1), intArrayOf(5)) + assertEquals(5, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 2, 5, 1, 3), result) + } + + @Test + fun testSquare2WithTail1Reverse() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(1, 0), intArrayOf(1, 0), intArrayOf(5)) + assertEquals(5, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(3, 1, 5, 2, 0), result) + } + + @Test + fun testSquare3WithFullTailAndReverseOrder() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(2, 1, 0), intArrayOf(2, 1, 0), intArrayOf(10, 11, 12)) + assertEquals(12, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(8, 5, 2, 10, 7, 4, 1, 11, 6, 3, 0, 12), result) + } + + @Test + fun test2x6andNoTail() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1), intArrayOf(0, 1, 2, 3, 4, 5), intArrayOf()) + assertEquals(12, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 6, 1, 7, 2, 8, 3, 9, 4, 10, 5, 11), result) + } + + @Test + fun test2x6and4Tail() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1), intArrayOf(0, 1, 2, 3, 4, 5), intArrayOf(20, 21, 22, 23)) + assertEquals(16, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 6, 20, 1, 7, 21, 2, 8, 22, 3, 9, 23, 4, 10, 5, 11), result) + } + + @Test + fun test6x2andNoTail() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1, 2, 3, 4, 5), intArrayOf(0, 1), intArrayOf()) + assertEquals(12, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 2, 4, 6, 8, 10, 1, 3, 5, 7, 9, 11), result) + } + + @Test + fun test6x2and4TailFailsBecauseItIsForbiddenSituation() { + assertThrows(IllegalStateException::class.java) { + PseudoShuffledIntProgression(intArrayOf(0, 1, 2, 3, 4, 5), intArrayOf(0, 1), intArrayOf(20, 21, 22, 23)) + } + } + + @Test + fun test6x2and1Tail() { + val shuffle = PseudoShuffledIntProgression(intArrayOf(0, 1, 2, 3, 4, 5), intArrayOf(0, 1), intArrayOf(20)) + assertEquals(13, shuffle.size) + val result = shuffle.toArray() + assertArrayEquals(intArrayOf(0, 2, 4, 6, 8, 10, 20, 1, 3, 5, 7, 9, 11), result) + } + + @Test + fun testIsShuffledAndNoDuplicatesForFirst1000() { + (2..1000).forEach { + val shuffle = PseudoShuffledIntProgression(it, Random(9)) + val result = (0 until shuffle.size).map(shuffle::get) + assertNotEquals(result.sorted(), result) + assertEquals(result.size, result.toSet().size) + } + } + + @Test + fun testIsRandom() { + val randomSeed = 10 + val size = 1000 + val expected = IntArray(size) { it }.apply { shuffle(Random(randomSeed)) } + val result = PseudoShuffledIntProgression(size, random = Random(randomSeed)) { it }.toArray() + + assertArrayEquals(expected, result) + } + + @Test + fun testForDifferentSides() { + val size = 10000 + (1 until size).forEach { side -> + val shuffle = PseudoShuffledIntProgression(size, Random) { side } + assertEquals(size, shuffle.size) + val values = shuffle.toArray().toSet() + assertEquals(values.size, shuffle.size) + } + } + + @ParameterizedTest(name = "testCorrectInternalArraysAreCreatedFor{arguments}DifferentSides") + @ValueSource(ints = [0, 1, 999, 1000]) + fun testCorrectInternalArraysAreCreatedForDifferentSides(size: Int) { + (1 until size / 2).forEach { side -> + val one = PseudoShuffledIntProgression(size, Random(98)) { side } + val two = PseudoShuffledIntProgression(size, Random(98)) { it / side } + assertEquals(one.size, two.size) + assertArrayEquals(one.toArray(), two.toArray()) { "Fails for side = $side" } + } + } + + @Test + fun testSpecification() { + val shuffle = PseudoShuffledIntProgression( + columns = intArrayOf(2, 1, 3, 0), + rows = intArrayOf(2, 3, 4, 0, 1), + tail = intArrayOf(22, 20, 21) + ) + assertArrayEquals( + intArrayOf(12, 7, 17, 2, 22, 13, 8, 18, 3, 20, 14, 9, 19, 4, 21, 10, 5, 15, 0, 11, 6, 16, 1), + shuffle.toArray() + ) + } + + @Test + fun testIterator() { + val shuffle = PseudoShuffledIntProgression( + columns = intArrayOf(2, 1, 3, 0), + rows = intArrayOf(2, 3, 4, 0, 1), + tail = intArrayOf(22, 20, 21) + ) + val expected = intArrayOf(12, 7, 17, 2, 22, 13, 8, 18, 3, 20, 14, 9, 19, 4, 21, 10, 5, 15, 0, 11, 6, 16, 1) + shuffle.forEachIndexed { index, value -> + assertEquals(expected[index], value) + } + } + + @Test + fun testIteratorIteratesAllValuesExclusively() { + val progression = PseudoShuffledIntProgression( + columns = intArrayOf(0, 1, 2, 3), + rows = intArrayOf(0, 1, 2), + tail = intArrayOf(12) + ) + val iterated = mutableListOf() + val iterator = progression.iterator() + while (iterator.hasNext()) { + iterated.add(iterator.nextInt()) + } + assertEquals(4 * 3 + 1, iterated.size) + assertEquals((0 until 4 * 3 + 1).toList(), iterated.sorted()) + } + + @Test + fun testNoIntOverflowWhenCalculateValue() { + assertDoesNotThrow { + PseudoShuffledIntProgression(Int.MAX_VALUE)[2147479015] + PseudoShuffledIntProgression(Int.MAX_VALUE)[Int.MAX_VALUE - 1] + } + } + + @Test + fun testNoDuplicates() { + val size = 2000 + val set = PseudoShuffledIntProgression(size).toSet() + assertEquals(size, set.size) + } +} \ No newline at end of file diff --git a/utbot-gradle/build.gradle b/utbot-gradle/build.gradle index 2b9cb3b008..b115420101 100644 --- a/utbot-gradle/build.gradle +++ b/utbot-gradle/build.gradle @@ -1,33 +1,97 @@ plugins { id 'java-gradle-plugin' + id 'com.gradle.plugin-publish' version '0.18.0' + id 'com.github.johnrengelman.shadow' version '6.1.0' } apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" +configurations { + fetchInstrumentationJar +} + dependencies { - api project(":utbot-framework") - api project(':utbot-summary') + shadow gradleApi() + shadow localGroovy() + + implementation project(":utbot-framework") + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration: 'instrumentationArchive') implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version } +// needed to prevent inclusion of gradle-api into shadow JAR +configurations.compile.dependencies.remove dependencies.gradleApi() + configurations.all { exclude group: "org.apache.logging.log4j", module: "log4j-slf4j-impl" } -gradlePlugin { - plugins { - sarifReportPlugin { - id = 'org.utbot.sarif' - implementationClass = 'org.utbot.sarif.SarifGradlePlugin' - } +jar { + manifest { + // 'Fat JAR' is needed in org.utbot.framework.codegen.model.util.DependencyUtilsKt.checkDependencyIsFatJar + attributes 'JAR-Type': 'Fat JAR' + attributes 'Class-Path': configurations.compile.collect { it.getName() }.join(' ') + } +} + +/** + * Shadow plugin unpacks the nested `utbot-instrumentation-.jar`. + * But we need it to be packed. Workaround: double-nest the jar. + */ +task shadowBugWorkaround(type: Jar) { + destinationDir file('build/shadow-bug-workaround') + from(configurations.fetchInstrumentationJar) { + into "lib" } } +// Documentation: https://imperceptiblethoughts.com/shadow/ +shadowJar { + archiveClassifier.set('') + minimize() + from shadowBugWorkaround +} + +// no module metadata => no dependency on the `utbot-framework` +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + publishing { + publications { + pluginMaven(MavenPublication) { + pom.withXml { + // removing a dependency to `utbot-framework` from the list of dependencies + asNode().dependencies.dependency.each { dependency -> + if (dependency.artifactId[0].value()[0] == 'utbot-framework') { + assert dependency.parent().remove(dependency) + } + } + } + } + } repositories { maven { url = layout.buildDirectory.dir('repo') } } } + +pluginBundle { + website = 'https://www.utbot.org/' + vcsUrl = 'https://github.com/UnitTestBot/UTBotJava/' + tags = ['java', 'unit-testing', 'tests-generation', 'sarif'] +} + +gradlePlugin { + plugins { + sarifReportPlugin { + version = '1.0.0-alpha-9' // last published version + id = 'org.utbot.gradle.plugin' + displayName = 'UnitTestBot gradle plugin' + description = 'The gradle plugin for generating tests and creating SARIF reports based on UnitTestBot' + implementationClass = 'org.utbot.gradle.plugin.SarifGradlePlugin' + } + } +} \ No newline at end of file diff --git a/utbot-gradle/docs/utbot-gradle.md b/utbot-gradle/docs/utbot-gradle.md index 31116201e8..819bb6f71a 100644 --- a/utbot-gradle/docs/utbot-gradle.md +++ b/utbot-gradle/docs/utbot-gradle.md @@ -2,107 +2,72 @@ Utbot Gradle Plugin is a gradle plugin for generating tests and creating SARIF-reports. -The `createSarifReport` gradle task generates tests and SARIF-reports for all classes in your project (or only for classes specified in the configuration). +The `generateTestsAndSarifReport` gradle task generates tests and SARIF-reports for all classes in your project (or only for classes specified in the configuration). In addition, it creates one big SARIF-report containing the results from all the processed files. ### How to use -- Add our nexus repository to your build file: - -
    - Groovy -
    -  buildscript {
    -      repositories {
    -          maven {
    -              url "http://[your-ip]:[your-port]/repository/utbot-uber/"
    -              allowInsecureProtocol true
    -          }
    -      }
    +Please, check for the available versions [here](https://plugins.gradle.org/plugin/org.utbot.gradle.plugin). 
    +
    +- Apply the plugin:
    +
    +  __Groovy:__
    +  ```Groovy
    +  plugins {
    +      id "org.utbot.gradle.plugin" version "..."
       }
    -  
    -
    - -
    - Kotlin DSL -
    -  buildscript {
    -      repositories {
    -          maven {
    -              url = uri("http://[your-ip]:[your-port]/repository/utbot-uber/")
    -              isAllowInsecureProtocol = true
    -          }
    -      }
    +  ```
    +  __Kotlin DSL:__
    +  ```Kotlin
    +  plugins {
    +      id("org.utbot.gradle.plugin") version "..."
       }
    -  
    -
    - -- Then apply the plugin: - -
    - Groovy -
    -  apply plugin: 'org.utbot.sarif'
    -  
    -
    - -
    - Kotlin DSL -
    -  apply(plugin = "org.utbot.sarif")
    -  
    -
    - -- Run gradle task `utbot/createSarifReport` to create a report. + ``` + +- Run gradle task `utbot/generateTestsAndSarifReport` to create a report. ### How to configure For example, the following configuration may be used: -
    -Groovy -
    -sarifReport {
    -    targetClasses = ['com.abc.Main', 'com.qwerty.Util']
    -    projectRoot = 'C:/.../SomeDirectory'
    -    generatedTestsRelativeRoot = 'build/generated/test'
    -    sarifReportsRelativeRoot = 'build/generated/sarif'
    -    markGeneratedTestsDirectoryAsTestSourcesRoot = true
    -    generationTimeout = 60000L
    -    testFramework = 'junit5'
    -    mockFramework = 'mockito'
    -    codegenLanguage = 'java'
    -    mockStrategy = 'package-based'
    -    staticsMocking = 'mock-statics'
    -    forceStaticMocking = 'force'
    -    classesToMockAlways = ['org.slf4j.Logger', 'java.util.Random']
    -}
    -
    -
    - - -
    -Kotlin DSL -
    -configure<SarifGradleExtension> {
    -    targetClasses.set(listOf("com.abc.Main", "com.qwerty.Util"))
    -    projectRoot.set("C:/.../SomeDirectory")
    -    generatedTestsRelativeRoot.set("build/generated/test")
    -    sarifReportsRelativeRoot.set("build/generated/sarif")
    -    markGeneratedTestsDirectoryAsTestSourcesRoot.set(true)
    -    generationTimeout.set(60000L)
    -    testFramework.set("junit5")
    -    mockFramework.set("mockito")
    -    codegenLanguage.set("java")
    -    mockStrategy.set("package-based")
    -    staticsMocking.set("mock-statics")
    -    forceStaticMocking.set("force")
    -    classesToMockAlways.set(listOf("org.slf4j.Logger", "java.util.Random"))
    -}
    -
    -
    +__Groovy:__ + ```Groovy + sarifReport { + targetClasses = ['com.abc.Main', 'com.qwerty.Util'] + projectRoot = 'C:/.../SomeDirectory' + generatedTestsRelativeRoot = 'build/generated/test' + sarifReportsRelativeRoot = 'build/generated/sarif' + markGeneratedTestsDirectoryAsTestSourcesRoot = true + testFramework = 'junit5' + mockFramework = 'mockito' + generationTimeout = 60000L + codegenLanguage = 'java' + mockStrategy = 'other-packages' + staticsMocking = 'mock-statics' + forceStaticMocking = 'force' + classesToMockAlways = ['org.slf4j.Logger', 'java.util.Random'] + } + ``` +__Kotlin DSL:__ + ```Kotlin + configure { + targetClasses.set(listOf("com.abc.Main", "com.qwerty.Util")) + projectRoot.set("C:/.../SomeDirectory") + generatedTestsRelativeRoot.set("build/generated/test") + sarifReportsRelativeRoot.set("build/generated/sarif") + markGeneratedTestsDirectoryAsTestSourcesRoot.set(true) + testFramework.set("junit5") + mockFramework.set("mockito") + generationTimeout.set(60000L) + codegenLanguage.set("java") + mockStrategy.set("other-packages") + staticsMocking.set("mock-statics") + forceStaticMocking.set("force") + classesToMockAlways.set(listOf("org.slf4j.Logger", "java.util.Random")) + } + ``` **Note:** All configuration fields have default values, so there is no need to configure the plugin if you don't want to. @@ -128,10 +93,6 @@ configure<SarifGradleExtension> { - Mark the directory with generated tests as `test sources root` or not. - By default, `true` is used. -- `generationTimeout` – - - Time budget for generating tests for one class (in milliseconds). - - By default, 60 seconds is used. - - `testFramework` – - The name of the test framework to be used. - Can be one of: @@ -144,6 +105,10 @@ configure<SarifGradleExtension> { - Can be one of: - `'mockito'` _(by default)_ +- `generationTimeout` – + - Time budget for generating tests for one class (in milliseconds). + - By default, 60 seconds is used. + - `codegenLanguage` – - The language of the generated tests. - Can be one of: @@ -153,9 +118,9 @@ configure<SarifGradleExtension> { - `mockStrategy` – - The mock strategy to be used. - Can be one of: - - `'do-not-mock'` – do not use mock frameworks at all - - `'package-based'` – mock all classes outside the current package except system ones _(by default)_ - - `'all-except-cut'` – mock all classes outside the class under test except system ones + - `'no-mocks'` – do not use mock frameworks at all + - `'other-packages'` – mock all classes outside the current package except system ones _(by default)_ + - `'other-classes'` – mock all classes outside the class under test except system ones - `staticsMocking` – - Use static methods mocking or not. @@ -179,114 +144,68 @@ configure<SarifGradleExtension> { If you want to change the source code of the plugin or even the whole utbot-project, you need to do the following: -- Publish the modified project to the local maven repository -- Correctly specify the dependencies in the build file (in your project) - -There are two ways to do it. - -- **The first way** - - Run `publishing/publishToMavenLocal` (**utbot root** gradle task) - - - Add to your build file: - -
    - Groovy -
    -      buildscript {
    -          repositories {
    -              mavenLocal()
    -              maven {
    -                  url "http://[your-ip]:[your-port]/repository/utbot-uber/"
    -                  allowInsecureProtocol true
    -              }
    -              mavenCentral()
    -          }
    -       
    -          dependencies {
    -              classpath group: 'org.utbot', name: 'utbot-gradle', version: '1.0-SNAPSHOT'
    -          }
    +
    +- Publish plugin to the local maven repository:  
    +  `utbot-gradle/publishing/publishToMavenLocal`
    +
    +- Add to your build file:
    +
    +  __Groovy:__
    +  ```Groovy
    +  buildscript {
    +      repositories {
    +          mavenLocal()
           }
    -      
    -
    - -
    - Kotlin DSL -
    -      buildscript {
    -          repositories {
    -              mavenLocal()
    -              maven {
    -                  url = uri("http://[your-ip]:[your-port]/repository/utbot-uber/")
    -                  isAllowInsecureProtocol = true
    -              }
    -              mavenCentral()
    -          }
    -       
    -          dependencies {
    -              classpath("org.utbot:utbot-gradle:1.0-SNAPSHOT")
    -          }
    +      dependencies {
    +          classpath "org.utbot:utbot-gradle:1.0-SNAPSHOT"
           }
    -      
    -
    - -- **The second way** (faster, but more difficult) - - Run `publishing/publishToMavenLocal` (**utbot-gradle** gradle task) - - Add to your `build.gradle`: - -
    - Groovy -
    -      buildscript {
    -          repositories {
    -              mavenLocal()
    -              maven {
    -                  url "http://[your-ip]:[your-port]/repository/utbot-uber/"
    -                  allowInsecureProtocol true
    -              }
    -              mavenCentral()
    -          }
    -       
    -          dependencies {
    -              classpath group: 'org.utbot', name: 'utbot-gradle', version: '1.0-SNAPSHOT'
    -              classpath files('C:/..[your-path]../UTBotJava/utbot-framework/build/libs/utbot-framework-1.0-SNAPSHOT.jar')
    -              classpath files('C:/..[your-path]../UTBotJava/utbot-framework-api/build/libs/utbot-framework-api-1.0-SNAPSHOT.jar')
    -              classpath files('C:/..[your-path]../UTBotJava/utbot-instrumentation/build/libs/utbot-instrumentation-1.0-SNAPSHOT.jar')
    -          }
    +  }
    +  ```
    +  __Kotlin DSL:__
    +  ```Kotlin
    +  buildscript {
    +      repositories {
    +          mavenLocal()
           }
    -      
    -
    - -
    - Kotlin DSL -
    -      buildscript {
    -          repositories {
    -              mavenLocal()
    -              maven {
    -                  url = uri("http://[your-ip]:[your-port]/repository/utbot-uber/")
    -                  isAllowInsecureProtocol = true
    -              }
    -              mavenCentral()
    -          }
    -       
    -          dependencies {
    -              classpath("org.utbot:utbot-gradle:1.0-SNAPSHOT")
    -              classpath(files("C:/..[your-path]../UTBotJava/utbot-framework/build/libs/utbot-framework-1.0-SNAPSHOT.jar"))
    -              classpath(files("C:/..[your-path]../UTBotJava/utbot-framework-api/build/libs/utbot-framework-api-1.0-SNAPSHOT.jar"))
    -              classpath(files("C:/..[your-path]../UTBotJava/utbot-instrumentation/build/libs/utbot-instrumentation-1.0-SNAPSHOT.jar"))
    -          }
    +      dependencies {
    +          classpath("org.utbot:utbot-gradle:1.0-SNAPSHOT")
           }
    -      
    -
    + } + ``` + +- Apply the plugin: + + __Groovy:__ + ```Groovy + apply plugin: 'org.utbot.gradle.plugin' + ``` + __Kotlin DSL:__ + ```Kotlin + apply(plugin = "org.utbot.gradle.plugin") + ``` ### How to configure the log level -To change the log level run the `createSarifReport` task with the appropriate flag. +To change the log level run the `generateTestsAndSarifReport` task with the appropriate flag. -For example, `createSarifReport --debug` +For example, `generateTestsAndSarifReport --debug` Note that the internal gradle log information will also be shown. Also note that the standard way to configure the log level (using the `log4j2.xml`) does not work from gradle. -[Read more about gradle log levels](https://docs.gradle.org/current/userguide/logging.html) \ No newline at end of file +[Read more about gradle log levels](https://docs.gradle.org/current/userguide/logging.html) + +### Publishing + +1. Read the [documentation](https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html) about plugin publishing +2. Sign in to our [account](https://plugins.gradle.org/u/utbot) to get API keys (if you don't have a password, please contact [Nikita Stroganov](https://github.com/IdeaSeeker)) +3. Run `utbot-gradle/plugin portal/publishPlugins` gradle task + +You can check the published artifacts in the [remote repository](https://plugins.gradle.org/m2/org/utbot/utbot-gradle/). + +Please note that the maximum archive size for publishing on the Gradle Plugin Portal is ~60Mb. + +### Requirements + +UTBot gradle plugin requires Gradle 6.8+ diff --git a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/GenerateTestsAndSarifReportTask.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/GenerateTestsAndSarifReportTask.kt new file mode 100644 index 0000000000..4d0259ed5f --- /dev/null +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/GenerateTestsAndSarifReportTask.kt @@ -0,0 +1,117 @@ +package org.utbot.gradle.plugin + +import mu.KLogger +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.utbot.common.bracket +import org.utbot.common.debug +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.framework.plugin.sarif.GenerateTestsAndSarifReportFacade +import org.utbot.gradle.plugin.extension.SarifGradleExtensionProvider +import org.utbot.gradle.plugin.wrappers.GradleProjectWrapper +import org.utbot.gradle.plugin.wrappers.SourceFindingStrategyGradle +import org.utbot.gradle.plugin.wrappers.SourceSetWrapper +import org.utbot.framework.plugin.sarif.TargetClassWrapper +import javax.inject.Inject + +/** + * The main class containing the entry point [generateTestsAndSarifReport]. + * + * [Documentation](https://docs.gradle.org/current/userguide/custom_tasks.html) + */ +open class GenerateTestsAndSarifReportTask @Inject constructor( + private val sarifProperties: SarifGradleExtensionProvider +) : DefaultTask() { + + init { + group = "utbot" + description = "Generate a SARIF report" + } + + /** + * Entry point: called when the user starts this gradle task. + */ + @TaskAction + fun generateTestsAndSarifReport() { + val rootGradleProject = try { + GradleProjectWrapper(project, sarifProperties) + } catch (t: Throwable) { + logger.error(t) { "Unexpected error while configuring the gradle task" } + return + } + try { + generateForProjectRecursively(rootGradleProject) + GenerateTestsAndSarifReportFacade.mergeReports( + sarifReports = rootGradleProject.collectReportsRecursively(), + mergedSarifReportFile = rootGradleProject.sarifReportFile + ) + } catch (t: Throwable) { + logger.error(t) { "Unexpected error while generating SARIF report" } + return + } + } + + // internal + + // overwriting the getLogger() function from the DefaultTask + private val logger: KLogger = org.utbot.gradle.plugin.logger + + /** + * Generates tests and a SARIF report for classes in the [gradleProject] and in all its child projects. + */ + private fun generateForProjectRecursively(gradleProject: GradleProjectWrapper) { + gradleProject.sourceSets.forEach { sourceSet -> + generateForSourceSet(sourceSet) + } + gradleProject.childProjects.forEach { childProject -> + generateForProjectRecursively(childProject) + } + } + + /** + * Generates tests and a SARIF report for classes in the [sourceSet]. + */ + private fun generateForSourceSet(sourceSet: SourceSetWrapper) { + logger.debug().bracket("Generating tests for the '${sourceSet.sourceSet.name}' source set") { + withUtContext(UtContext(sourceSet.classLoader)) { + sourceSet.targetClasses.forEach { targetClass -> + generateForClass(sourceSet, targetClass) + } + } + } + } + + /** + * Generates tests and a SARIF report for the class [targetClass]. + */ + private fun generateForClass(sourceSet: SourceSetWrapper, targetClass: TargetClassWrapper) { + logger.debug().bracket("Generating tests for the $targetClass") { + val sourceFindingStrategy = + SourceFindingStrategyGradle(sourceSet, targetClass.testsCodeFile.path) + val generateTestsAndSarifReportFacade = + GenerateTestsAndSarifReportFacade(sarifProperties, sourceFindingStrategy) + generateTestsAndSarifReportFacade.generateForClass( + targetClass, sourceSet.workingDirectory, sourceSet.runtimeClasspath + ) + } + } + + /** + * Returns SARIF reports created for this [GradleProjectWrapper] and for all its child projects. + */ + private fun GradleProjectWrapper.collectReportsRecursively(): List = + this.sourceSets.flatMap { sourceSetWrapper -> + sourceSetWrapper.collectReports() + } + this.childProjects.flatMap { childProject -> + childProject.collectReportsRecursively() + } + + /** + * Returns SARIF reports created for this [SourceSetWrapper]. + */ + private fun SourceSetWrapper.collectReports(): List = + this.targetClasses.map { targetClass -> + targetClass.sarifReportFile.readText() + } +} diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/SarifGradlePlugin.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/SarifGradlePlugin.kt similarity index 80% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/SarifGradlePlugin.kt rename to utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/SarifGradlePlugin.kt index e190eb772d..0940353d95 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/SarifGradlePlugin.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/SarifGradlePlugin.kt @@ -1,14 +1,14 @@ -package org.utbot.sarif +package org.utbot.gradle.plugin -import org.utbot.sarif.extension.SarifGradleExtension -import org.utbot.sarif.extension.SarifGradleExtensionProvider -import java.io.File +import mu.KotlinLogging import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.SourceSet import org.gradle.api.tasks.TaskProvider -import mu.KotlinLogging +import org.utbot.gradle.plugin.extension.SarifGradleExtension +import org.utbot.gradle.plugin.extension.SarifGradleExtensionProvider +import java.io.File internal val logger = KotlinLogging.logger {} @@ -28,9 +28,9 @@ class SarifGradlePlugin : Plugin { /** * The name of the gradle task. - * @see [CreateSarifReportTask] + * @see [GenerateTestsAndSarifReportTask] */ - internal val createSarifReportTaskName = "createSarifReport" + internal val generateTestsAndSarifReportTaskName = "generateTestsAndSarifReport" /** * Entry point: called when the plugin is applied. @@ -45,12 +45,12 @@ class SarifGradlePlugin : Plugin { sarifGradleExtension ) - val createSarifReportTask = project.tasks.register( - createSarifReportTaskName, - CreateSarifReportTask::class.java, + val generateTestsAndSarifReportTask = project.tasks.register( + generateTestsAndSarifReportTaskName, + GenerateTestsAndSarifReportTask::class.java, sarifGradleExtensionProvider ) - createSarifReportTask.addDependencyOnClassesTasksRecursively(project) + generateTestsAndSarifReportTask.addDependencyOnClassesTasksRecursively(project) markGeneratedTestsDirectoryIfNeededRecursively(project, sarifGradleExtensionProvider) } @@ -60,7 +60,7 @@ class SarifGradlePlugin : Plugin { /** * Applies [addDependencyOnClassesTasks] to the [project] and to all its child projects. */ - private fun TaskProvider.addDependencyOnClassesTasksRecursively(project: Project) { + private fun TaskProvider.addDependencyOnClassesTasksRecursively(project: Project) { project.afterEvaluate { addDependencyOnClassesTasks(project) } @@ -70,13 +70,13 @@ class SarifGradlePlugin : Plugin { } /** - * Makes [CreateSarifReportTask] dependent on `classes` task + * Makes [GenerateTestsAndSarifReportTask] dependent on `classes` task * of each source set from the [project], except `test` source set. * * The [project] should be evaluated because we need its `java` plugin. * Therefore, it is recommended to call this function in the `project.afterEvaluate` block. */ - private fun TaskProvider.addDependencyOnClassesTasks(project: Project) { + private fun TaskProvider.addDependencyOnClassesTasks(project: Project) { val javaPlugin = project.convention.findPlugin(JavaPluginConvention::class.java) if (javaPlugin == null) { logger.warn { @@ -88,11 +88,11 @@ class SarifGradlePlugin : Plugin { sourceSet.name != SourceSet.TEST_SOURCE_SET_NAME } logger.debug { "Found source sets in the '${project.name}': ${sourceSetsExceptTest.map { it.name }}" } - configure { createSarifReportTask -> + configure { generateTestsAndSarifReportTask -> sourceSetsExceptTest.map { sourceSet -> val classesTask = project.tasks.getByName(sourceSet.classesTaskName) - createSarifReportTask.dependsOn(classesTask) - logger.debug { "'createSarifReport' task now depends on the task '${classesTask.name}'" } + generateTestsAndSarifReportTask.dependsOn(classesTask) + logger.debug { "'generateTestsAndSarifReport' task now depends on the task '${classesTask.name}'" } } } } diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtension.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt similarity index 95% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtension.kt rename to utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt index 394ffd3c6e..1ebbc1dfda 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtension.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtension.kt @@ -1,4 +1,4 @@ -package org.utbot.sarif.extension +package org.utbot.gradle.plugin.extension import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property @@ -68,7 +68,7 @@ abstract class SarifGradleExtension { abstract val codegenLanguage: Property /** - * Can be one of: 'do-not-mock', 'package-based', 'all-except-cut'. + * Can be one of: 'no-mocks', 'other-packages', 'other-classes'. */ @get:Input abstract val mockStrategy: Property @@ -105,7 +105,7 @@ sarifReport { mockFramework = 'mockito' generationTimeout = 60 * 1000L codegenLanguage = 'java' - mockStrategy = 'do-not-mock' + mockStrategy = 'no-mocks' staticsMocking = 'do-not-mock-statics' forceStaticMocking = 'force' classesToMockAlways = ['org.utbot.api.mock.UtMock'] diff --git a/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProvider.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProvider.kt new file mode 100644 index 0000000000..67c8e18d89 --- /dev/null +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProvider.kt @@ -0,0 +1,84 @@ +package org.utbot.gradle.plugin.extension + +import org.gradle.api.Project +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.StaticsMocking +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.sarif.SarifExtensionProvider +import java.io.File + +/** + * Provides all [SarifGradleExtension] fields in a convenient form: + * Defines default values and a transform function for these fields. + */ +class SarifGradleExtensionProvider( + private val project: Project, + private val extension: SarifGradleExtension +) : SarifExtensionProvider { + + override val targetClasses: List + get() = extension.targetClasses + .getOrElse(listOf()) + + override val projectRoot: File + get() = extension.projectRoot.orNull + ?.toPath()?.toFile() + ?: project.projectDir + + override val generatedTestsRelativeRoot: String + get() = extension.generatedTestsRelativeRoot.orNull + ?: "build/generated/test" + + override val sarifReportsRelativeRoot: String + get() = extension.sarifReportsRelativeRoot.orNull + ?: "build/generated/sarif" + + override val markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean + get() = extension.markGeneratedTestsDirectoryAsTestSourcesRoot.orNull + ?: true + + override val testFramework: TestFramework + get() = extension.testFramework + .map(::testFrameworkParse) + .getOrElse(TestFramework.defaultItem) + + override val mockFramework: MockFramework + get() = extension.mockFramework + .map(::mockFrameworkParse) + .getOrElse(MockFramework.defaultItem) + + override val generationTimeout: Long + get() = extension.generationTimeout + .map(::generationTimeoutParse) + .getOrElse(60 * 1000L) // 60 seconds + + override val codegenLanguage: CodegenLanguage + get() = extension.codegenLanguage + .map(::codegenLanguageParse) + .getOrElse(CodegenLanguage.defaultItem) + + override val mockStrategy: MockStrategyApi + get() = extension.mockStrategy + .map(::mockStrategyParse) + .getOrElse(MockStrategyApi.defaultItem) + + override val staticsMocking: StaticsMocking + get() = extension.staticsMocking + .map(::staticsMockingParse) + .getOrElse(StaticsMocking.defaultItem) + + override val forceStaticMocking: ForceStaticMocking + get() = extension.forceStaticMocking + .map(::forceStaticMockingParse) + .getOrElse(ForceStaticMocking.defaultItem) + + override val classesToMockAlways: Set + get() = classesToMockAlwaysParse( + extension.classesToMockAlways.getOrElse(listOf()) + ) +} diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapper.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapper.kt similarity index 92% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapper.kt rename to utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapper.kt index e79aba126f..2a7c7748f1 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapper.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapper.kt @@ -1,12 +1,12 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers -import org.utbot.common.FileUtil.createNewFileWithParentDirectories -import org.utbot.sarif.extension.SarifGradleExtensionProvider -import java.io.File -import java.nio.file.Paths import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.SourceSet +import org.utbot.common.FileUtil.createNewFileWithParentDirectories +import org.utbot.gradle.plugin.extension.SarifGradleExtensionProvider +import java.io.File +import java.nio.file.Paths /** * Contains information about the gradle project for which we are creating a SARIF report. @@ -59,7 +59,7 @@ class GradleProjectWrapper( val sarifReportFile: File by lazy { Paths.get( generatedSarifDirectory.path, - "${project.name}-utbot.sarif" + "${project.name}Report.sarif" ).toFile().apply { createNewFileWithParentDirectories() } diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradle.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt similarity index 97% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradle.kt rename to utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt index 9d09c8b8ad..e45a190a17 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradle.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradle.kt @@ -1,4 +1,4 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers import org.utbot.common.PathUtil import org.utbot.common.PathUtil.toPath diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceSetWrapper.kt b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapper.kt similarity index 54% rename from utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceSetWrapper.kt rename to utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapper.kt index 2f78023680..47dff001b8 100644 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/wrappers/SourceSetWrapper.kt +++ b/utbot-gradle/src/main/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapper.kt @@ -1,20 +1,17 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers +import org.gradle.api.tasks.SourceSet import org.utbot.common.FileUtil.createNewFileWithParentDirectories import org.utbot.common.FileUtil.findAllFilesOnly import org.utbot.common.PathUtil.classFqnToPath import org.utbot.common.PathUtil.replaceSeparator -import org.utbot.common.loadClassesFromDirectory import org.utbot.common.tryLoadClass -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter.Companion.computeSourceFileName +import org.utbot.framework.plugin.sarif.util.ClassUtil +import org.utbot.framework.plugin.sarif.TargetClassWrapper import java.io.File import java.net.URLClassLoader import java.nio.file.Path import java.nio.file.Paths -import org.gradle.api.tasks.SourceSet class SourceSetWrapper( val sourceSet: SourceSet, @@ -27,7 +24,7 @@ class SourceSetWrapper( val runtimeClasspath: String by lazy { sourceSet.runtimeClasspath.filter { file -> file.exists() - }.joinToString(separator = ";") { file -> + }.joinToString(File.pathSeparator) { file -> replaceSeparator(file.absolutePath) } } @@ -53,36 +50,22 @@ class SourceSetWrapper( /** * List of declared in the [sourceSet] classes. - * Does not contain classes without declared methods or without a canonicalName. */ val targetClasses: List by lazy { - parentProject.sarifProperties.targetClasses.ifEmpty { - // finds all declared classes if `sarifProperties.targetClasses` is empty - classLoader - .loadClassesFromDirectory( - classesDirectory = workingDirectory.toFile() - ) - .filter { clazz -> - clazz.canonicalName != null && clazz.declaredMethods.isNotEmpty() - } - .map { clazz -> - clazz.canonicalName - } - }.mapNotNull { classFqn -> - constructTargetClassWrapper(classFqn) - } + parentProject.sarifProperties.targetClasses + .ifEmpty { + ClassUtil.findAllDeclaredClasses(classLoader, workingDirectory.toFile()) + } + .mapNotNull { classFqn -> + constructTargetClassWrapper(classFqn) + } } /** * Finds the source code file by the given class fully qualified name. */ fun findSourceCodeFile(classFqn: String): File? = - sourceCodeFiles.firstOrNull { sourceCodeFile -> - val relativePath = "${classFqnToPath(classFqn)}.${sourceCodeFile.extension}" - sourceCodeFile.endsWith(File(relativePath)) - } ?: classLoader.tryLoadClass(classFqn)?.let { clazz: Class<*> -> - findSourceCodeFileByClass(clazz) - } + ClassUtil.findSourceCodeFile(classFqn, sourceCodeFiles, classLoader) // internal @@ -113,49 +96,22 @@ class SourceSetWrapper( /** * Creates and returns a file for a future SARIF report. - * For example, ".../main/com/qwerty/Main-utbot.sarif". + * For example, ".../com/qwerty/MainReport.sarif". */ private fun createSarifReportFile(classFqn: String): File { - val relativePath = "${sourceSet.name}/${classFqnToPath(classFqn)}-utbot.sarif" + val relativePath = "${classFqnToPath(classFqn)}Report.sarif" val absolutePath = Paths.get(parentProject.generatedSarifDirectory.path, relativePath) return absolutePath.toFile().apply { createNewFileWithParentDirectories() } } /** * Creates and returns a file for future generated tests. - * For example, ".../java/main/com/qwerty/MainTest.java". + * For example, ".../com/qwerty/MainTest.java". */ private fun createTestsCodeFile(classFqn: String): File { val fileExtension = parentProject.sarifProperties.codegenLanguage.extension - val sourceRoot = parentProject.sarifProperties.codegenLanguage.toSourceRootName() - val relativePath = "$sourceRoot/${sourceSet.name}/${classFqnToPath(classFqn)}Test$fileExtension" + val relativePath = "${classFqnToPath(classFqn)}Test$fileExtension" val absolutePath = Paths.get(parentProject.generatedTestsDirectory.path, relativePath) return absolutePath.toFile().apply { createNewFileWithParentDirectories() } } - - /** - * Fallback logic: called after a failure of [findSourceCodeFile]. - */ - private fun findSourceCodeFileByClass(clazz: Class<*>): File? { - val sourceFileName = withUtContext(UtContext(classLoader)) { - computeSourceFileName(clazz) // finds the file name in bytecode - } ?: return null - val candidates = sourceCodeFiles.filter { sourceCodeFile -> - sourceCodeFile.endsWith(File(sourceFileName)) - } - return if (candidates.size == 1) - candidates.first() - else // we can't decide which file is needed - null - } - - /** - * Returns the source root name by [CodegenLanguage]. - */ - private fun CodegenLanguage.toSourceRootName(): String = - when (this) { - CodegenLanguage.JAVA -> "java" - CodegenLanguage.KOTLIN -> "kotlin" - else -> "unknown" - } } \ No newline at end of file diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/CreateSarifReportTask.kt b/utbot-gradle/src/main/kotlin/org/utbot/sarif/CreateSarifReportTask.kt deleted file mode 100644 index 003e8dc39c..0000000000 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/CreateSarifReportTask.kt +++ /dev/null @@ -1,190 +0,0 @@ -package org.utbot.sarif - -import org.utbot.common.bracket -import org.utbot.common.debug -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.model.ModelBasedTestCodeGenerator -import org.utbot.framework.plugin.api.UtBotTestCaseGenerator -import org.utbot.framework.plugin.api.UtTestCase -import org.utbot.framework.plugin.api.util.UtContext -import org.utbot.framework.plugin.api.util.withUtContext -import org.utbot.sarif.extension.SarifGradleExtensionProvider -import org.utbot.sarif.wrappers.GradleProjectWrapper -import org.utbot.sarif.wrappers.SourceFindingStrategyGradle -import org.utbot.sarif.wrappers.SourceSetWrapper -import org.utbot.sarif.wrappers.TargetClassWrapper -import org.utbot.summary.summarize -import java.net.URLClassLoader -import java.nio.file.Path -import javax.inject.Inject -import mu.KLogger -import org.gradle.api.DefaultTask -import org.gradle.api.tasks.TaskAction - -/** - * The main class containing the entry point [createSarifReport]. - * - * [Documentation](https://docs.gradle.org/current/userguide/custom_tasks.html) - */ -open class CreateSarifReportTask @Inject constructor( - private val sarifProperties: SarifGradleExtensionProvider -) : DefaultTask() { - - init { - group = "utbot" - description = "Generate a SARIF report" - } - - /** - * Entry point: called when the user starts this gradle task. - */ - @TaskAction - fun createSarifReport() { - val rootGradleProject = try { - GradleProjectWrapper(project, sarifProperties) - } catch (t: Throwable) { - logger.error(t) { "Unexpected error while configuring the gradle task" } - return - } - try { - generateForProjectRecursively(rootGradleProject) - mergeReports(rootGradleProject) - } catch (t: Throwable) { - logger.error(t) { "Unexpected error while generating SARIF report" } - return - } - } - - // internal - - // overwriting the getLogger() function from the DefaultTask - private val logger: KLogger = org.utbot.sarif.logger - - /** - * Generates tests and a SARIF report for classes in the [gradleProject] and in all its child projects. - */ - private fun generateForProjectRecursively(gradleProject: GradleProjectWrapper) { - gradleProject.sourceSets.forEach { sourceSet -> - generateForSourceSet(sourceSet) - } - gradleProject.childProjects.forEach { childProject -> - generateForProjectRecursively(childProject) - } - } - - /** - * Generates tests and a SARIF report for classes in the [sourceSet]. - */ - private fun generateForSourceSet(sourceSet: SourceSetWrapper) { - logger.debug().bracket("Generating tests for the '${sourceSet.sourceSet.name}' source set") { - // UtContext is used in `generateTestCases` and `generateTestCode` - withUtContext(UtContext(sourceSet.classLoader)) { - sourceSet.targetClasses.forEach { targetClass -> - generateForClass(sourceSet, targetClass) - } - } - } - } - - /** - * Generates tests and a SARIF report for the class [targetClass]. - */ - private fun generateForClass(sourceSet: SourceSetWrapper, targetClass: TargetClassWrapper) { - logger.debug().bracket("Generating tests for the $targetClass") { - initializeEngine(sourceSet.runtimeClasspath, sourceSet.workingDirectory) - - val testCases = generateTestCases(targetClass, sourceSet.workingDirectory) - val testClassBody = generateTestCode(targetClass, testCases) - targetClass.testsCodeFile.writeText(testClassBody) - - generateReport(sourceSet, targetClass, testCases, testClassBody) - } - } - - private val dependencyPaths by lazy { - val thisClassLoader = this::class.java.classLoader as URLClassLoader - thisClassLoader.urLs.joinToString(separator = ";") { it.path } - } - - private fun initializeEngine(classPath: String, workingDirectory: Path) { - UtBotTestCaseGenerator.init(workingDirectory, classPath, dependencyPaths) { false } - } - - private fun generateTestCases(targetClass: TargetClassWrapper, workingDirectory: Path): List = - UtBotTestCaseGenerator.generateForSeveralMethods( - targetClass.targetMethods(), - sarifProperties.mockStrategy, - sarifProperties.classesToMockAlways, - sarifProperties.generationTimeout - ).map { - it.summarize(targetClass.sourceCodeFile, workingDirectory) - } - - private fun generateTestCode(targetClass: TargetClassWrapper, testCases: List): String = - initializeCodeGenerator(targetClass) - .generateAsString(testCases, targetClass.testsCodeFile.nameWithoutExtension) - - private fun initializeCodeGenerator(targetClass: TargetClassWrapper) = - ModelBasedTestCodeGenerator().apply { - val isNoStaticMocking = sarifProperties.staticsMocking is NoStaticMocking - val isForceStaticMocking = sarifProperties.forceStaticMocking == ForceStaticMocking.FORCE - init( - classUnderTest = targetClass.classUnderTest.java, - testFramework = sarifProperties.testFramework, - mockFramework = sarifProperties.mockFramework, - staticsMocking = sarifProperties.staticsMocking, - forceStaticMocking = sarifProperties.forceStaticMocking, - generateWarningsForStaticMocking = isNoStaticMocking && isForceStaticMocking, - codegenLanguage = sarifProperties.codegenLanguage - ) - } - - // SARIF reports - - /** - * Creates a SARIF report for the class [targetClass]. - * Saves the report to the file specified in [targetClass]. - */ - private fun generateReport( - sourceSet: SourceSetWrapper, - targetClass: TargetClassWrapper, - testCases: List, - testClassBody: String - ) { - logger.debug().bracket("Creating a SARIF report for the $targetClass") { - val sourceFinding = SourceFindingStrategyGradle(sourceSet, targetClass.testsCodeFile.path) - val sarifReport = SarifReport(testCases, testClassBody, sourceFinding).createReport() - targetClass.sarifReportFile.writeText(sarifReport) - } - } - - /** - * Returns SARIF reports created for this [GradleProjectWrapper] and for all its child projects. - */ - private fun GradleProjectWrapper.collectReportsRecursively(): List = - this.sourceSets.flatMap { sourceSetWrapper -> - sourceSetWrapper.collectReports() - } + this.childProjects.flatMap { childProject -> - childProject.collectReportsRecursively() - } - - /** - * Returns SARIF reports created for this [SourceSetWrapper]. - */ - private fun SourceSetWrapper.collectReports(): List = - this.targetClasses.map { targetClass -> - targetClass.sarifReportFile.readText() - } - - /** - * Merges all SARIF reports into one large containing all the information. - */ - private fun mergeReports(gradleProject: GradleProjectWrapper) { - val reports = gradleProject.collectReportsRecursively() - val mergedReport = SarifReport.mergeReports(reports) - gradleProject.sarifReportFile.writeText(mergedReport) - println("SARIF report was saved to \"${gradleProject.sarifReportFile.path}\"") - println("You can open it using the VS Code extension \"Sarif Viewer\"") - } -} diff --git a/utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProvider.kt b/utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProvider.kt deleted file mode 100644 index a42201f1ec..0000000000 --- a/utbot-gradle/src/main/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProvider.kt +++ /dev/null @@ -1,165 +0,0 @@ -package org.utbot.sarif.extension - -import org.utbot.common.PathUtil.toPath -import org.utbot.engine.Mocker -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.Junit5 -import org.utbot.framework.codegen.MockitoStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.TestNg -import org.utbot.framework.plugin.api.ClassId -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.MockStrategyApi -import java.io.File -import org.gradle.api.Project - -/** - * Provides all [SarifGradleExtension] fields in a convenient form: - * Defines default values and a transform function for these fields. - */ -class SarifGradleExtensionProvider( - private val project: Project, - private val extension: SarifGradleExtension -) { - - /** - * Classes for which the SARIF report will be created. - */ - val targetClasses: List - get() = extension.targetClasses - .getOrElse(listOf()) - - /** - * Absolute path to the root of the relative paths in the SARIF report. - */ - val projectRoot: File - get() = extension.projectRoot.orNull - ?.toPath()?.toFile() - ?: project.projectDir - - /** - * Relative path to the root of the generated tests. - */ - val generatedTestsRelativeRoot: String - get() = extension.generatedTestsRelativeRoot.orNull - ?: "build/generated/test" - - /** - * Relative path to the root of the SARIF reports. - */ - val sarifReportsRelativeRoot: String - get() = extension.sarifReportsRelativeRoot.orNull - ?: "build/generated/sarif" - - /** - * Mark the directory with generated tests as `test sources root` or not. - */ - val markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean - get() = extension.markGeneratedTestsDirectoryAsTestSourcesRoot.orNull - ?: true - - val testFramework: TestFramework - get() = extension.testFramework - .map(::testFrameworkParse) - .getOrElse(TestFramework.defaultItem) - - val mockFramework: MockFramework - get() = extension.mockFramework - .map(::mockFrameworkParse) - .getOrElse(MockFramework.defaultItem) - - /** - * Maximum tests generation time for one class (in milliseconds). - */ - val generationTimeout: Long - get() = extension.generationTimeout - .map(::generationTimeoutParse) - .getOrElse(60 * 1000L) // 60 seconds - - val codegenLanguage: CodegenLanguage - get() = extension.codegenLanguage - .map(::codegenLanguageParse) - .getOrElse(CodegenLanguage.defaultItem) - - val mockStrategy: MockStrategyApi - get() = extension.mockStrategy - .map(::mockStrategyParse) - .getOrElse(MockStrategyApi.defaultItem) - - val staticsMocking: StaticsMocking - get() = extension.staticsMocking - .map(::staticsMockingParse) - .getOrElse(StaticsMocking.defaultItem) - - val forceStaticMocking: ForceStaticMocking - get() = extension.forceStaticMocking - .map(::forceStaticMockingParse) - .getOrElse(ForceStaticMocking.defaultItem) - - /** - * Contains user-specified classes and `Mocker.defaultSuperClassesToMockAlwaysNames`. - */ - val classesToMockAlways: Set - get() { - val defaultClasses = Mocker.defaultSuperClassesToMockAlwaysNames - val specifiedClasses = extension.classesToMockAlways.getOrElse(listOf()) - return (defaultClasses + specifiedClasses).map { className -> - ClassId(className) - }.toSet() - } - - // transform functions - - private fun testFrameworkParse(testFramework: String): TestFramework = - when (testFramework.toLowerCase()) { - "junit4" -> Junit4 - "junit5" -> Junit5 - "testng" -> TestNg - else -> error("Parameter testFramework == '$testFramework', but it can take only 'junit4', 'junit5' or 'testng'") - } - - private fun mockFrameworkParse(mockFramework: String): MockFramework = - when (mockFramework.toLowerCase()) { - "mockito" -> MockFramework.MOCKITO - else -> error("Parameter mockFramework == '$mockFramework', but it can take only 'mockito'") - } - - private fun generationTimeoutParse(generationTimeout: Long): Long { - if (generationTimeout < 0) - error("Parameter generationTimeout == $generationTimeout, but it should be non-negative") - return generationTimeout - } - - private fun codegenLanguageParse(codegenLanguage: String): CodegenLanguage = - when (codegenLanguage.toLowerCase()) { - "java" -> CodegenLanguage.JAVA - "kotlin" -> CodegenLanguage.KOTLIN - else -> error("Parameter codegenLanguage == '$codegenLanguage', but it can take only 'java' or 'kotlin'") - } - - private fun mockStrategyParse(mockStrategy: String): MockStrategyApi = - when (mockStrategy.toLowerCase()) { - "do-not-mock" -> MockStrategyApi.NO_MOCKS - "package-based" -> MockStrategyApi.OTHER_PACKAGES - "all-except-cut" -> MockStrategyApi.OTHER_CLASSES - else -> error("Parameter mockStrategy == '$mockStrategy', but it can take only 'do-not-mock', 'package-based' or 'all-except-cut'") - } - - private fun staticsMockingParse(staticsMocking: String): StaticsMocking = - when (staticsMocking.toLowerCase()) { - "do-not-mock-statics" -> NoStaticMocking - "mock-statics" -> MockitoStaticMocking - else -> error("Parameter staticsMocking == '$staticsMocking', but it can take only 'do-not-mock-statics' or 'mock-statics'") - } - - private fun forceStaticMockingParse(forceStaticMocking: String): ForceStaticMocking = - when (forceStaticMocking.toLowerCase()) { - "force" -> ForceStaticMocking.FORCE - "do-not-force" -> ForceStaticMocking.DO_NOT_FORCE - else -> error("Parameter forceStaticMocking == '$forceStaticMocking', but it can take only 'force' or 'do-not-force'") - } -} diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradlePluginTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradlePluginTest.kt similarity index 73% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradlePluginTest.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradlePluginTest.kt index 062e7f43d8..6451c75f8a 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradlePluginTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradlePluginTest.kt @@ -1,4 +1,4 @@ -package org.utbot.sarif +package org.utbot.gradle.plugin import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test @@ -13,11 +13,11 @@ class SarifGradlePluginTest { } @Test - fun `plugin should register createSarifReport task`() { + fun `plugin should register generateTestsAndSarifReport task`() { val project = buildProject() val sarifGradlePlugin = project.sarifGradlePlugin - val createSarifReportTask = project.tasks.getByName(sarifGradlePlugin.createSarifReportTaskName) - assertNotNull(createSarifReportTask) + val generateTestsAndSarifReportTask = project.tasks.getByName(sarifGradlePlugin.generateTestsAndSarifReportTaskName) + assertNotNull(generateTestsAndSarifReportTask) } @Test diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradleTestUtil.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradleTestUtil.kt similarity index 88% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradleTestUtil.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradleTestUtil.kt index 9f98304da9..1ac588475f 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/SarifGradleTestUtil.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/SarifGradleTestUtil.kt @@ -1,4 +1,4 @@ -package org.utbot.sarif +package org.utbot.gradle.plugin import org.gradle.api.Project import org.gradle.api.plugins.JavaPluginConvention @@ -8,7 +8,7 @@ import org.gradle.testfixtures.ProjectBuilder internal fun buildProject(): Project { val project = ProjectBuilder.builder().build() project.pluginManager.apply("java") - project.pluginManager.apply("org.utbot.sarif") + project.pluginManager.apply("org.utbot.gradle.plugin") return project } diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProviderTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt similarity index 95% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProviderTest.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt index b3d9fb8f71..0bc54f1c3e 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/extension/SarifGradleExtensionProviderTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/extension/SarifGradleExtensionProviderTest.kt @@ -1,27 +1,20 @@ -package org.utbot.sarif.extension +package org.utbot.gradle.plugin.extension +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito import org.utbot.common.PathUtil.toPath import org.utbot.engine.Mocker -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.Junit5 -import org.utbot.framework.codegen.MockitoStaticMocking -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.TestNg +import org.utbot.framework.codegen.* import org.utbot.framework.plugin.api.ClassId import org.utbot.framework.plugin.api.CodegenLanguage import org.utbot.framework.plugin.api.MockFramework import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.sarif.buildProject +import org.utbot.gradle.plugin.buildProject import java.io.File -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Nested -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.mockito.Mockito class SarifGradleExtensionProviderTest { @@ -263,19 +256,19 @@ class SarifGradleExtensionProviderTest { @Test fun `should be equal to NO_MOCKS`() { - setMockStrategy("do-not-mock") + setMockStrategy("no-mocks") assertEquals(MockStrategyApi.NO_MOCKS, extensionProvider.mockStrategy) } @Test fun `should be equal to OTHER_PACKAGES`() { - setMockStrategy("package-based") + setMockStrategy("other-packages") assertEquals(MockStrategyApi.OTHER_PACKAGES, extensionProvider.mockStrategy) } @Test fun `should be equal to OTHER_CLASSES`() { - setMockStrategy("all-except-cut") + setMockStrategy("other-classes") assertEquals(MockStrategyApi.OTHER_CLASSES, extensionProvider.mockStrategy) } diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapperTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapperTest.kt similarity index 94% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapperTest.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapperTest.kt index 4ba75428e8..8eb307b023 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/GradleProjectWrapperTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/GradleProjectWrapperTest.kt @@ -1,17 +1,15 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers -import org.utbot.sarif.buildProject -import org.utbot.sarif.extension.SarifGradleExtensionProvider -import java.io.File import org.gradle.api.Project import org.gradle.api.tasks.SourceSet -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.mockito.Mockito +import org.utbot.gradle.plugin.buildProject +import org.utbot.gradle.plugin.extension.SarifGradleExtensionProvider +import java.io.File class GradleProjectWrapperTest { diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradleTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradleTest.kt similarity index 98% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradleTest.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradleTest.kt index 718ff3730f..2accab875d 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceFindingStrategyGradleTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceFindingStrategyGradleTest.kt @@ -1,12 +1,12 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers -import org.utbot.common.PathUtil.toPath -import java.nio.file.Paths import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.mockito.Mockito +import org.utbot.common.PathUtil.toPath +import java.nio.file.Paths class SourceFindingStrategyGradleTest { diff --git a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceSetWrapperTest.kt b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapperTest.kt similarity index 93% rename from utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceSetWrapperTest.kt rename to utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapperTest.kt index f640c21f22..5a98a7354c 100644 --- a/utbot-gradle/src/test/kotlin/org/utbot/sarif/wrappers/SourceSetWrapperTest.kt +++ b/utbot-gradle/src/test/kotlin/org/utbot/gradle/plugin/wrappers/SourceSetWrapperTest.kt @@ -1,20 +1,19 @@ -package org.utbot.sarif.wrappers +package org.utbot.gradle.plugin.wrappers -import org.utbot.common.FileUtil.createNewFileWithParentDirectories -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.util.* -import org.utbot.sarif.buildProject -import org.utbot.sarif.extension.SarifGradleExtensionProvider -import org.utbot.sarif.mainSourceSet -import java.nio.file.Paths import org.gradle.api.Project -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.mockito.Mockito +import org.utbot.common.FileUtil.createNewFileWithParentDirectories +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.util.Snippet +import org.utbot.framework.util.compileClassFile +import org.utbot.gradle.plugin.buildProject +import org.utbot.gradle.plugin.extension.SarifGradleExtensionProvider +import org.utbot.gradle.plugin.mainSourceSet +import java.nio.file.Paths class SourceSetWrapperTest { diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestConstructors.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestConstructors.kt new file mode 100644 index 0000000000..93801a9621 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestConstructors.kt @@ -0,0 +1,141 @@ +package org.utbot.examples + +import org.utbot.examples.samples.ClassWithMultipleConstructors +import org.utbot.examples.samples.ClassWithSameMethodNames +import org.utbot.framework.plugin.api.util.signature +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.instrumentation.et.ExecutionTraceInstrumentation +import org.utbot.instrumentation.instrumentation.et.convert +import org.utbot.instrumentation.instrumentation.et.function +import org.utbot.instrumentation.instrumentation.et.invoke +import org.utbot.instrumentation.instrumentation.et.pass +import org.utbot.instrumentation.instrumentation.et.ret +import org.utbot.instrumentation.withInstrumentation +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class TestConstructors { + lateinit var utContext: AutoCloseable + + private val CLASSPATH = ClassWithSameMethodNames::class.java.protectionDomain.codeSource.location.path + + @Test + fun testDefaultConstructor() { + ConcreteExecutor( + InvokeInstrumentation(), + CLASSPATH + ).use { executor -> + val constructors = ClassWithMultipleConstructors::class.constructors + val constr = constructors.first { it.parameters.isEmpty() } + val res = executor.execute(constr, arrayOf()) + val checkClass = ClassWithMultipleConstructors() + assertEquals(checkClass, res.getOrNull()) + assertFalse(checkClass === res.getOrNull()) + } + } + + @Test + fun testIntConstructors() { + ConcreteExecutor( + InvokeInstrumentation(), + CLASSPATH + ).use { executor -> + val constructors = ClassWithMultipleConstructors::class.constructors + + val constrI = constructors.first { it.signature == "(I)V" } + val resI = executor.execute(constrI, arrayOf(1)) + assertEquals(ClassWithMultipleConstructors(1), resI.getOrNull()) + + val constrII = constructors.first { it.signature == "(II)V" } + val resII = executor.execute(constrII, arrayOf(1, 2)) + assertEquals(ClassWithMultipleConstructors(3), resII.getOrNull()) + + val constrIII = constructors.first { it.signature == "(III)V" } + val resIII = executor.execute(constrIII, arrayOf(1, 2, 3)) + assertEquals(ClassWithMultipleConstructors(6), resIII.getOrNull()) + } + } + + @Test + fun testStringConstructors() { + withInstrumentation( + InvokeInstrumentation(), + CLASSPATH + ) { executor -> + val constructors = ClassWithMultipleConstructors::class.constructors + + val constrSS = constructors.first { it.parameters.size == 2 && it.signature != "(II)V" } + val resSS = executor.execute(constrSS, arrayOf("100", "23")) + assertEquals(ClassWithMultipleConstructors(123), resSS.getOrNull()) + + val constrS = constructors.first { it.parameters.size == 1 && it.signature != "(I)V" } + val resS1 = executor.execute(constrS, arrayOf("one")) + assertEquals(ClassWithMultipleConstructors(1), resS1.getOrNull()) + + val resS2 = executor.execute(constrS, arrayOf("kek")) + assertEquals(ClassWithMultipleConstructors(-1), resS2.getOrNull()) + } + } + + @Test + fun testCoverageConstructor() { + withInstrumentation( + CoverageInstrumentation, + CLASSPATH + ) { executor -> + val constructors = ClassWithMultipleConstructors::class.constructors + + val constrIII = constructors.first { it.signature == "(III)V" } + executor.execute(constrIII, arrayOf(1, 2, 3)) + + val coverage = executor.collectCoverage(ClassWithMultipleConstructors::class.java) + val method2instr = coverage.methodToInstrRange + assertTrue(method2instr["()V"]!!.minus(coverage.visitedInstrs).isEmpty()) + assertTrue(method2instr["(I)V"]!!.minus(coverage.visitedInstrs).isEmpty()) + assertTrue(method2instr["(II)V"]!!.minus(coverage.visitedInstrs).isEmpty()) + assertTrue(method2instr["(III)V"]!!.minus(coverage.visitedInstrs).toList() == (36..40).toList()) + } + } + + @Test + fun testExecutionTraceConstructor() { + withInstrumentation( + ExecutionTraceInstrumentation(), + CLASSPATH + ) { executor -> + val constructors = ClassWithMultipleConstructors::class.constructors + + val constrIII = constructors.first { it.signature == "(III)V" } + val trace = executor.execute(constrIII, arrayOf(1, 2, 3)) + + assertEquals( + function("(III)V") { + pass() + invoke("(II)V") { + pass() + invoke("(I)V") { + pass() + invoke("()V") { + pass() + ret() + } + pass() + ret() + } + pass() + ret() + } + pass() + ret() + }, + convert(trace) + ) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt new file mode 100644 index 0000000000..b99cacb5b4 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestCoverageInstrumentation.kt @@ -0,0 +1,196 @@ +package org.utbot.examples + +import org.utbot.examples.samples.ExampleClass +import org.utbot.examples.statics.substitution.StaticSubstitution +import org.utbot.examples.statics.substitution.StaticSubstitutionExamples +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.framework.plugin.api.util.signature +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.util.ChildProcessError +import org.utbot.instrumentation.util.StaticEnvironment +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class TestCoverageInstrumentation { + lateinit var utContext: AutoCloseable + + @Test + fun testCatchTargetException() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val res = it.execute(ExampleClass::kek2, arrayOf(testObject, 123)) + val coverageInfo = it.collectCoverage(ExampleClass::class.java) + + assertEquals(5, coverageInfo.visitedInstrs.size) + assertEquals(50..55, coverageInfo.methodToInstrRange[ExampleClass::kek2.signature]) + assertTrue(res.exceptionOrNull() is ArrayIndexOutOfBoundsException) + } + } + + @Test + fun testIfBranches() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + it.execute(ExampleClass::bar, arrayOf(testObject, 2)) + val coverageInfo1 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(21, coverageInfo1.visitedInstrs.size) + assertEquals(13..49, coverageInfo1.methodToInstrRange[ExampleClass::bar.signature]) + + it.execute(ExampleClass::bar, arrayOf(testObject, 0)) + val coverageInfo2 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(20, coverageInfo2.visitedInstrs.size) + assertEquals(13..49, coverageInfo2.methodToInstrRange[ExampleClass::bar.signature]) + } + } + + @Test + fun testWrongArgumentsException() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + val exc = assertThrows { + it.execute( + ExampleClass::bar, + arrayOf(testObject, 1, 2, 3) + ) + } + + assertInstanceOf( + IllegalArgumentException::class.java, + exc.cause!! + ) + } + } + + + @Test + fun testMultipleRunsInsideCoverage() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + val exc = assertThrows { + it.execute( + ExampleClass::bar, + arrayOf(testObject, 1, 2, 3) + ) + } + + assertInstanceOf( + IllegalArgumentException::class.java, + exc.cause!! + ) + + it.execute(ExampleClass::bar, arrayOf(testObject, 2)) + val coverageInfo1 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(21, coverageInfo1.visitedInstrs.size) + assertEquals(13..49, coverageInfo1.methodToInstrRange[ExampleClass::bar.signature]) + + it.execute(ExampleClass::bar, arrayOf(testObject, 0)) + val coverageInfo2 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(20, coverageInfo2.visitedInstrs.size) + assertEquals(13..49, coverageInfo2.methodToInstrRange[ExampleClass::bar.signature]) + } + } + + + @Test + fun testSameResult() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + it.execute(ExampleClass::dependsOnField, arrayOf(testObject)) + val coverageInfo1 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(19, coverageInfo1.visitedInstrs.size) + assertEquals(99..124, coverageInfo1.methodToInstrRange[ExampleClass::dependsOnField.signature]) + + it.execute(ExampleClass::dependsOnField, arrayOf(testObject)) + val coverageInfo2 = it.collectCoverage(ExampleClass::class.java) + + assertEquals(19, coverageInfo2.visitedInstrs.size) + assertEquals(99..124, coverageInfo2.methodToInstrRange[ExampleClass::dependsOnField.signature]) + } + } + + @Test + fun testResult() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val res = it.execute(ExampleClass::foo, arrayOf(testObject, 3)) + val coverageInfo = it.collectCoverage(ExampleClass::class.java) + + assertEquals(1, res.getOrNull()) + assertEquals(35, coverageInfo.visitedInstrs.size) + assertEquals(56..98, coverageInfo.methodToInstrRange[ExampleClass::foo.signature]) + } + } + + @Test + fun testEmptyMethod() { + ConcreteExecutor( + CoverageInstrumentation, + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val res = it.execute(ExampleClass::emptyMethod, arrayOf(testObject)) + val coverageInfo = it.collectCoverage(ExampleClass::class.java) + + assertEquals(Unit::class, res.getOrNull()!!::class) + assertEquals(1, coverageInfo.visitedInstrs.size) + } + } + + @Test + fun testTernaryOperator() { + ConcreteExecutor( + CoverageInstrumentation, + StaticSubstitutionExamples::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = StaticSubstitutionExamples() + + val emptyStaticEnvironment = StaticEnvironment() + + val res1 = it.execute(StaticSubstitutionExamples::lessThanZero, arrayOf(testObject), parameters = emptyStaticEnvironment) + + val staticEnvironment = StaticEnvironment( + StaticSubstitution::mutableValue.fieldId to -1 + ) + val res2 = it.execute(StaticSubstitutionExamples::lessThanZero, arrayOf(testObject), parameters = staticEnvironment) + val coverageInfo = it.collectCoverage(StaticSubstitutionExamples::class.java) + + assertEquals(res1.getOrNull(), 5) + assertEquals(res2.getOrNull(), 0) + assertEquals(coverageInfo.visitedInstrs, (3..10).toList()) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestGetSourceFileName.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestGetSourceFileName.kt new file mode 100644 index 0000000000..bb256f024a --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestGetSourceFileName.kt @@ -0,0 +1,104 @@ +package org.utbot.examples + +import ClassWithoutPackage +import org.utbot.examples.samples.ClassWithInnerClasses +import org.utbot.examples.samples.ExampleClass +import org.utbot.examples.samples.wrongpackage.ClassWithWrongPackage +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.instrumentation.instrumentation.instrumenter.Instrumenter +import java.nio.file.Paths +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class TestGetSourceFileName { + private lateinit var cookie: AutoCloseable + + @BeforeEach + fun setup() { + cookie = UtContext.setUtContext(UtContext(ClassLoader.getSystemClassLoader())) + } + + @AfterEach + fun tearDown() { + cookie.close() + } + + @Test + fun testThis() { + assertEquals("TestGetSourceFileName.kt", Instrumenter.computeSourceFileName(TestGetSourceFileName::class.java)) + } + + @Test + fun testKotlinExample() { + assertEquals("ExampleClass.kt", Instrumenter.computeSourceFileName(ExampleClass::class.java)) + } + + @Test + fun testJavaExample() { + assertEquals( + "ClassWithInnerClasses.java", + Instrumenter.computeSourceFileName(ClassWithInnerClasses::class.java) + ) + } + + @Test + fun testInnerClass() { + assertEquals( + "ClassWithInnerClasses.java", + Instrumenter.computeSourceFileName(ClassWithInnerClasses.InnerStaticClass::class.java) + ) + } + + @Test + fun testSameNameButDifferentPackages() { + assertEquals( + true, + Instrumenter.computeSourceFileByClass(org.utbot.examples.samples.root.MyClass::class.java)?.toPath() + ?.endsWith(Paths.get("root", "MyClass.java")) + ) + assertEquals( + true, + Instrumenter.computeSourceFileByClass(org.utbot.examples.samples.root.child.MyClass::class.java) + ?.toPath()?.endsWith(Paths.get("root", "child", "MyClass.java")) + ) + } + + @Test + fun testEmptyPackage() { + assertEquals( + true, + Instrumenter.computeSourceFileByClass(ClassWithoutPackage::class.java)?.toPath() + ?.endsWith("java/ClassWithoutPackage.java") + ) + } + + @Test + fun testPackageDoesNotMatchDir() { + assertEquals( + true, + Instrumenter.computeSourceFileByClass(ClassWithWrongPackage::class.java)?.toPath() + ?.endsWith("org/utbot/examples/samples/ClassWithWrongPackage.kt") + ) + } + + @Test + fun testSearchDir() { + assertEquals( + null, + Instrumenter.computeSourceFileByClass( + org.utbot.examples.samples.root.MyClass::class.java, + Paths.get("src/test/kotlin") + )?.name + ) + + assertEquals( + "MyClass.java", + Instrumenter.computeSourceFileByClass( + org.utbot.examples.samples.root.MyClass::class.java, + Paths.get("src/test") + )?.name + ) + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt new file mode 100644 index 0000000000..9b0b06669e --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeInstrumentation.kt @@ -0,0 +1,156 @@ +package org.utbot.examples + +import org.utbot.examples.samples.ClassWithSameMethodNames +import org.utbot.examples.samples.ExampleClass +import org.utbot.examples.samples.staticenvironment.StaticExampleClass +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.util.ChildProcessError +import kotlin.reflect.full.declaredMembers +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +class TestInvokeInstrumentation { + lateinit var utContext: AutoCloseable + + @Test + fun testCatchTargetException() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + + val testObject = ExampleClass() + + val res = it.execute(ExampleClass::kek2, arrayOf(testObject, 123)) + + assertTrue(res.exceptionOrNull() is ArrayIndexOutOfBoundsException) + } + } + + @Test + fun testWrongArgumentsException() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + val exc = assertThrows { + it.execute( + ExampleClass::bar, + arrayOf(testObject, 1, 2, 3) + ) + } + assertInstanceOf( + IllegalArgumentException::class.java, + exc.cause!! + ) + } + } + + @Test + fun testSameResult() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val res1 = it.execute(ExampleClass::dependsOnFieldReturn, arrayOf(testObject)) + assertEquals(2, res1.getOrNull()) + + val res2 = it.execute(ExampleClass::dependsOnFieldReturn, arrayOf(testObject)) + assertEquals(2, res2.getOrNull()) + } + } + + @Test + fun testEmptyMethod() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val res = it.execute(ExampleClass::emptyMethod, arrayOf(testObject)) + + assertEquals(Unit::class, res.getOrNull()!!::class) + } + } + + @Test + fun testStaticMethodCall() { + ConcreteExecutor( + InvokeInstrumentation(), + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val res1 = it.execute(StaticExampleClass::inc, arrayOf()) + assertEquals(0, res1.getOrNull()) + + val res2 = it.execute( + StaticExampleClass::plus, + arrayOf(5) + ) + + assertEquals(0, res2.getOrNull()) + } + } + + @Test + fun testNullableMethod() { + ConcreteExecutor( + InvokeInstrumentation(), + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val res1 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(10, + "123") + ) + assertEquals("123", res1.getOrNull()) + + val res2 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(0, + "kek") + ) + + assertEquals(null, res2.getOrNull()) + + val res3 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(1, + null) + ) + + assertEquals(null, res3.getOrNull()) + } + } + + @Test + fun testDifferentSignaturesButSameMethodNames() { + ConcreteExecutor( + InvokeInstrumentation(), + ClassWithSameMethodNames::class.java.protectionDomain.codeSource.location.path + ).use { + val clazz = ClassWithSameMethodNames::class + + val sumVararg = clazz.declaredMembers.first { it.parameters.size == 1 } + val sum2 = clazz.declaredMembers.first { it.parameters.size == 2 } + val sum3 = clazz.declaredMembers.first { it.parameters.size == 3 } + + val resVararg = it.execute(sumVararg, arrayOf(intArrayOf(1, 2, 3, 4, 5))) + assertEquals(Result.success(15), resVararg) + + val resSum2 = it.execute(sum2, arrayOf(1, 5)) + assertEquals(Result.success(8), resSum2) + + val resSum3 = it.execute(sum3, arrayOf(1, 5, 4)) + assertEquals(Result.success(13), resSum3) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeWithStaticsInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeWithStaticsInstrumentation.kt new file mode 100644 index 0000000000..b1865499e0 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestInvokeWithStaticsInstrumentation.kt @@ -0,0 +1,139 @@ +package org.utbot.examples + +import org.utbot.examples.samples.staticenvironment.InnerClass +import org.utbot.examples.samples.staticenvironment.MyHiddenClass +import org.utbot.examples.samples.staticenvironment.ReferenceEqualityExampleClass +import org.utbot.examples.samples.staticenvironment.StaticExampleClass +import org.utbot.examples.samples.staticenvironment.TestedClass +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.fieldId +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.InvokeWithStaticsInstrumentation +import org.utbot.instrumentation.util.StaticEnvironment +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class TestInvokeWithStaticsInstrumentation { + lateinit var utContext: AutoCloseable + + @BeforeEach + fun initContext() { + utContext = UtContext.setUtContext(UtContext(ClassLoader.getSystemClassLoader())) + } + + @AfterEach + fun closeConctext() { + utContext.close() + } + + val CLASSPATH = StaticExampleClass::class.java.protectionDomain.codeSource.location.path + + @Test + fun testIfBranches() { + ConcreteExecutor( + InvokeWithStaticsInstrumentation(), + CLASSPATH + ).use { + val res = it.execute(StaticExampleClass::inc, arrayOf(), null) + assertEquals(0, res.getOrNull()) + + val staticEnvironment = StaticEnvironment( + StaticExampleClass::digit.fieldId to 5, + ) + val resWithStatics = it.execute(StaticExampleClass::inc, arrayOf(), parameters = staticEnvironment) + assertEquals(1, resWithStatics.getOrNull()) + } + } + + @Test + fun testHiddenClass1() { + ConcreteExecutor( + InvokeWithStaticsInstrumentation(), + CLASSPATH + ).use { + val res = it.execute(TestedClass::slomayInts, arrayOf(), null) + assertEquals(12, res.getOrNull()) + + val se = StaticEnvironment( + TestedClass::x.fieldId to 0, + MyHiddenClass::var0.fieldId to 0 + ) + val resWithStatics = it.execute(TestedClass::slomayInts, arrayOf(), parameters = se) + assertEquals(2, resWithStatics.getOrNull()) + } + } + + @Disabled("Question: What to do when user hasn't provided all the used static fields?") + @Test + fun testHiddenClassRepeatCall() { + ConcreteExecutor( + InvokeWithStaticsInstrumentation(), + CLASSPATH + ).use { + val se = StaticEnvironment( + TestedClass::x.fieldId to 0, + MyHiddenClass::var0.fieldId to 0 + ) + val resWithStatics = it.execute(TestedClass::slomayInts, arrayOf(), parameters = se) + assertEquals(2, resWithStatics.getOrNull()) + + val resAgain = it.execute(TestedClass::slomayInts, arrayOf(), null) + assertEquals(12, resAgain.getOrNull()) + } + } + + @Test + fun testReferenceEquality() { + ConcreteExecutor( + InvokeWithStaticsInstrumentation(), + CLASSPATH + ).use { + + val thisObject = ReferenceEqualityExampleClass() + + val res12 = it.execute(ReferenceEqualityExampleClass::test12, arrayOf(thisObject), null) + val res23 = it.execute(ReferenceEqualityExampleClass::test23, arrayOf(thisObject), null) + val res31 = it.execute(ReferenceEqualityExampleClass::test31, arrayOf(thisObject), null) + + assertEquals(true, res12.getOrNull()) + assertEquals(false, res23.getOrNull()) + assertEquals(false, res31.getOrNull()) + + val ic1 = InnerClass() + + val se = StaticEnvironment( + ReferenceEqualityExampleClass::field1.fieldId to ic1, + ReferenceEqualityExampleClass::field2.fieldId to ic1, + ReferenceEqualityExampleClass::field3.fieldId to ic1 + ) + + val res12_2 = it.execute(ReferenceEqualityExampleClass::test12, arrayOf(thisObject), parameters = se) + val res23_2 = it.execute(ReferenceEqualityExampleClass::test23, arrayOf(thisObject), parameters = se) + val res31_2 = it.execute(ReferenceEqualityExampleClass::test31, arrayOf(thisObject), parameters = se) + + assertEquals(true, res12_2.getOrNull()) + assertEquals(true, res23_2.getOrNull()) + assertEquals(true, res31_2.getOrNull()) + + val ic2 = InnerClass() + val ic3 = InnerClass() + + val se2 = StaticEnvironment( + ReferenceEqualityExampleClass::field1.fieldId to ic3, + ReferenceEqualityExampleClass::field2.fieldId to ic2, + ReferenceEqualityExampleClass::field3.fieldId to ic3 + ) + val res12_3 = it.execute(ReferenceEqualityExampleClass::test12, arrayOf(thisObject), parameters = se2) + val res23_3 = it.execute(ReferenceEqualityExampleClass::test23, arrayOf(thisObject), parameters = se2) + val res31_3 = it.execute(ReferenceEqualityExampleClass::test31, arrayOf(thisObject), parameters = se2) + + assertEquals(false, res12_3.getOrNull()) + assertEquals(false, res23_3.getOrNull()) + assertEquals(true, res31_3.getOrNull()) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt new file mode 100644 index 0000000000..c86695d4e0 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestIsolated.kt @@ -0,0 +1,135 @@ +package org.utbot.examples + +import org.utbot.examples.samples.ExampleClass +import org.utbot.examples.samples.staticenvironment.StaticExampleClass +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.util.ChildProcessError +import org.utbot.instrumentation.util.Isolated +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows + +class TestIsolated { + lateinit var utContext: AutoCloseable + + @Test + fun testCatchTargetException() { + val javaClass = ExampleClass::class.java + ConcreteExecutor( + InvokeInstrumentation(), + javaClass.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val isolatedFunction = Isolated(ExampleClass::kek2, it) + + val res = isolatedFunction(testObject, 123) + + assertTrue(res.exceptionOrNull() is ArrayIndexOutOfBoundsException) + } + } + + @Test + fun testWrongArgumentsException() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + val isolatedFunction = Isolated(ExampleClass::bar, it) + + assertDoesNotThrow { + isolatedFunction(testObject, 1) + } + + + val exc = assertThrows { + isolatedFunction(testObject, 1, 2, 3) + } + + assertInstanceOf( + IllegalArgumentException::class.java, + exc.cause!! + ) + } + } + + @Test + fun testSameResult() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val isolatedFunction = Isolated(ExampleClass::dependsOnFieldReturn, it) + + val res1 = isolatedFunction(testObject) + assertEquals(2, res1.getOrNull()) + + val res2 = isolatedFunction(testObject) + assertEquals(2, res2.getOrNull()) + } + } + + @Test + fun testEmptyMethod() { + ConcreteExecutor( + InvokeInstrumentation(), + ExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val testObject = ExampleClass() + + val isolatedFunction = Isolated(ExampleClass::emptyMethod, it) + + val res = isolatedFunction(testObject) + + assertEquals(Unit::class, res.getOrNull()!!::class) + } + } + + @Test + fun testStaticMethodCall() { + ConcreteExecutor( + InvokeInstrumentation(), + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val isolatedFunctionInc = Isolated(StaticExampleClass::inc, it) + + val res1 = isolatedFunctionInc() + assertEquals(0, res1.getOrNull()) + + val isolatedFunctionPlus = Isolated(StaticExampleClass::plus, it) + + val res2 = isolatedFunctionPlus(5) + + assertEquals(0, res2.getOrNull()) + } + } + + @Test + fun testNullableMethod() { + ConcreteExecutor( + InvokeInstrumentation(), + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val isolatedFunction = Isolated(StaticExampleClass::canBeNull, it) + + val res1 = isolatedFunction(10, "123") + + assertEquals("123", res1.getOrNull()) + + val res2 = isolatedFunction(0, "kek") + + assertEquals(null, res2.getOrNull()) + + val res3 = isolatedFunction(1, null) + + assertEquals(null, res3.getOrNull()) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestStaticMethods.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestStaticMethods.kt new file mode 100644 index 0000000000..6ad3bca179 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestStaticMethods.kt @@ -0,0 +1,105 @@ +package org.utbot.examples + +import org.utbot.examples.samples.staticenvironment.StaticExampleClass +import org.utbot.framework.plugin.api.util.signature +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + + +class TestStaticMethods { + lateinit var utContext: AutoCloseable + + @Test + fun testStaticMethodCall() { + ConcreteExecutor( + CoverageInstrumentation, + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val res1 = it.execute(StaticExampleClass::inc, arrayOf()) + val coverageInfo1 = it.collectCoverage(StaticExampleClass::class.java) + + assertEquals(0, res1.getOrNull()) + assertEquals((3..6).toList() + (9..22).toList(), coverageInfo1.visitedInstrs) + + val res2 = it.execute( + StaticExampleClass::plus, + arrayOf(5) + ) + val coverageInfo2 = it.collectCoverage(StaticExampleClass::class.java) + + assertEquals(0, res2.getOrNull()) + assertTrue( + coverageInfo2.methodToInstrRange[StaticExampleClass::plus.signature]!!.toList() + .subtract(coverageInfo2.visitedInstrs) + .isEmpty() + ) + } + } + + @Test + fun testNullableMethod() { + ConcreteExecutor( + CoverageInstrumentation, + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val res1 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(10, + "123") + ) + assertEquals("123", res1.getOrNull()) + + val res2 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(0, + "kek") + ) + + assertEquals(null, res2.getOrNull()) + + val res3 = it.execute( + StaticExampleClass::canBeNull, + arrayOf(1, + null) + ) + + assertEquals(null, res3.getOrNull()) + } + } + + @Test + fun testNullableMethodWithoutAnnotations() { + ConcreteExecutor( + CoverageInstrumentation, + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ).use { + val res1 = it.execute( + StaticExampleClass::canBeNullWithoutAnnotations, + arrayOf(10, + "123") + ) + assertEquals("123", res1.getOrNull()) + + val res2 = it.execute( + StaticExampleClass::canBeNullWithoutAnnotations, + arrayOf(0, + "kek") + ) + + assertEquals(null, res2.getOrNull()) + + val res3 = it.execute( + StaticExampleClass::canBeNullWithoutAnnotations, + arrayOf(1, + null) + ) + + assertEquals(null, res3.getOrNull()) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestWithInstrumentation.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestWithInstrumentation.kt new file mode 100644 index 0000000000..f49c010b81 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/TestWithInstrumentation.kt @@ -0,0 +1,90 @@ +package org.utbot.examples + +import org.utbot.examples.samples.ClassWithInnerClasses +import org.utbot.examples.samples.ClassWithSameMethodNames +import org.utbot.examples.samples.staticenvironment.StaticExampleClass +import org.utbot.framework.plugin.api.util.signature +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.withInstrumentation +import kotlin.reflect.full.declaredMembers +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TestWithInstrumentation { + lateinit var utContext: AutoCloseable + + @Test + fun testStaticMethodCall() { + withInstrumentation( + CoverageInstrumentation, + StaticExampleClass::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val res1 = executor.execute(StaticExampleClass::inc, arrayOf()) + val coverageInfo1 = executor.collectCoverage(StaticExampleClass::class.java) + + assertEquals(0, res1.getOrNull()) + assertEquals((3..6).toList() + (9..22).toList(), coverageInfo1.visitedInstrs) + + val res2 = executor.execute( + StaticExampleClass::plus, + arrayOf(5) + ) + val coverageInfo2 = executor.collectCoverage(StaticExampleClass::class.java) + + assertEquals(0, res2.getOrNull()) + Assertions.assertTrue( + coverageInfo2.methodToInstrRange[StaticExampleClass::plus.signature]!!.toList() + .subtract(coverageInfo2.visitedInstrs) + .isEmpty() + ) + } + } + + @Test + fun testDifferentSignaturesButSameMethodNames() { + withInstrumentation( + InvokeInstrumentation(), + ClassWithSameMethodNames::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val clazz = ClassWithSameMethodNames::class + + val sumVararg = clazz.declaredMembers.first { it.parameters.size == 1 } + val sum2 = clazz.declaredMembers.first { it.parameters.size == 2 } + val sum3 = clazz.declaredMembers.first { it.parameters.size == 3 } + + val resVararg = executor.execute(sumVararg, arrayOf(intArrayOf(1, 2, 3, 4, 5))) + assertEquals(Result.success(15), resVararg) + + val resSum2 = executor.execute(sum2, arrayOf(1, 5)) + assertEquals(Result.success(8), resSum2) + + val resSum3 = executor.execute(sum3, arrayOf(1, 5, 4)) + assertEquals(Result.success(13), resSum3) + } + } + + @Test + fun testInnerClasses() { + withInstrumentation( + CoverageInstrumentation, + ClassWithInnerClasses::class.java.protectionDomain.codeSource.location.path + ) { executor -> + val innerClazz = ClassWithInnerClasses.InnerClass::class.java + val innerStaticClazz = ClassWithInnerClasses.InnerStaticClass::class.java + + val classWithInnerClasses = ClassWithInnerClasses(5) + val res = executor.execute(ClassWithInnerClasses::doSomething, arrayOf(classWithInnerClasses, 1, 1)) + assertEquals(8, res.getOrNull()) + + val coverageInnerClass = executor.collectCoverage(innerClazz) + assertEquals((0..24).toList().minus(listOf(11, 12)), coverageInnerClass.visitedInstrs) + + val coverageInnerStaticClazz = executor.collectCoverage(innerStaticClazz) + assertEquals((3..6).toList(), coverageInnerStaticClazz.visitedInstrs) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Benchmark.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Benchmark.kt new file mode 100644 index 0000000000..41ec9da126 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Benchmark.kt @@ -0,0 +1,85 @@ +package org.utbot.examples.benchmark + +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.util.Isolated +import kotlin.system.measureNanoTime +import org.junit.jupiter.api.Assertions.assertEquals + + +fun getBasicCoverageTime(count: Int): Double { + var time: Long + ConcreteExecutor( + CoverageInstrumentation, + Repeater::class.java.protectionDomain.codeSource.location.path + ).use { executor -> + val dc0 = Repeater(", ") + val concat = Isolated(Repeater::concat, executor) + + for (i in 0..20000) { + val res = concat(dc0, "flex", "mega-", 10) + assertEquals("mega-mega-mega-mega-mega-mega-mega-mega-mega-mega-flex", res.getOrNull()) + } + + time = measureNanoTime { + for (i in 0..count) { + val res = concat(dc0, "flex", "mega-", 10) + assertEquals("mega-mega-mega-mega-mega-mega-mega-mega-mega-mega-flex", res.getOrNull()) + } + executor.collectCoverage(Repeater::class.java) + } + } + return time / 1e6 +} + +fun getNativeCallTime(count: Int): Double { + val dc0 = Repeater(", ") + for (i in 0..20000) { + val res0 = dc0.concat("flex", "mega-", 10) + assertEquals("mega-mega-mega-mega-mega-mega-mega-mega-mega-mega-flex", res0) + } + val time = measureNanoTime { + for (i in 0..count) { + dc0.concat("flex", "mega-", 10) + } + } + return time / 1e6 +} + +fun getJustResultTime(count: Int): Double { + var time: Long + ConcreteExecutor( + InvokeInstrumentation(), + Repeater::class.java.protectionDomain.codeSource.location.path + ).use { + val dc0 = Repeater(", ") + val concat = Isolated(Repeater::concat, it) + + for (i in 0..20000) { + val res = concat(dc0, "flex", "mega-", 10) + assertEquals("mega-mega-mega-mega-mega-mega-mega-mega-mega-mega-flex", res.getOrNull()) + } + + time = measureNanoTime { + for (i in 0..count) { + concat(dc0, "flex", "mega-", 10) + } + } + } + return time / 1e6 +} + +fun main() { + val callsCount = 400_000 + + val nativeCallTime = getNativeCallTime(callsCount) + val basicCoverageTime = getBasicCoverageTime(callsCount) + val justResultTime = getJustResultTime(callsCount) + + println("Running results on $callsCount method calls") + println("nativeCall: $nativeCallTime ms") + println("basicCoverage: $basicCoverageTime ms, overhead per call: ${(basicCoverageTime - nativeCallTime) / callsCount} ms") + println("justResult: $justResultTime ms, overhead per call: ${(justResultTime - nativeCallTime) / callsCount} ms") +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/BenchmarkFibonacci.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/BenchmarkFibonacci.kt new file mode 100644 index 0000000000..4c370553de --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/BenchmarkFibonacci.kt @@ -0,0 +1,81 @@ +package org.utbot.examples.benchmark + +import org.utbot.examples.samples.benchmark.Fibonacci +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.util.Isolated +import kotlin.system.measureNanoTime + +fun getBasicCoverageTime_fib(count: Int): Double { + var time: Long + ConcreteExecutor( + CoverageInstrumentation, + Fibonacci::class.java.protectionDomain.codeSource.location.path + ).use { + val fib = Isolated(Fibonacci::calc, it) + for (i in 0..20_000) { + fib(1, 1, 13) + } + + time = measureNanoTime { + for (i in 0..count) { + fib(1, 1, 13) + } + it.collectCoverage(Fibonacci::class.java) + } + } + return time / 1e6 +} + +fun getNativeCallTime_fib(count: Int): Double { + for (i in 0..20_000) { + Fibonacci.calc(1, 1, 13) + + } + val time = measureNanoTime { + for (i in 0..count) { + Fibonacci.calc(1, 1, 13) + } + } + return time / 1e6 +} + +fun getJustResultTime_fib(count: Int): Double { + var time: Long + ConcreteExecutor( + InvokeInstrumentation(), + Fibonacci::class.java.protectionDomain.codeSource.location.path + ).use { + val fib = Isolated(Fibonacci::calc, it) + + for (i in 0..20_000) { + fib(1, 1, 13) + } + + time = measureNanoTime { + for (i in 0..count) { + fib(1, 1, 13) + } + } + } + return time / 1e6 +} + +fun main() { + withUtContext(UtContext(ClassLoader.getSystemClassLoader())) { + val callsCount = 300_000 + + val nativeCallTime = getNativeCallTime_fib(callsCount) + val basicCoverageTime = getBasicCoverageTime_fib(callsCount) + val justResultTime = getJustResultTime_fib(callsCount) + + println("Running results on $callsCount method calls") + println("nativeCall: $nativeCallTime ms") + println("basicCoverage: $basicCoverageTime ms, overhead per call: ${(basicCoverageTime - nativeCallTime) / callsCount} ms") + println("justResult: $justResultTime ms, overhead per call: ${(justResultTime - nativeCallTime) / callsCount} ms") + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Classes.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Classes.kt new file mode 100644 index 0000000000..a4e0656e12 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/Classes.kt @@ -0,0 +1,32 @@ +package org.utbot.examples.benchmark + +import javafx.util.Pair + +class Repeater(var sep: String) { + /* public DifferentClass0() { + this.sep = "-"; + }*/ + fun repeat(str: String?, times: Int): String { + return concat(sep, str, times) + } + + fun concat(x: String?, y: String?, times: Int): String { + val sb = StringBuilder() + for (i in 0 until times) { + sb.append(y) + } + sb.append(x) + return sb.toString() + } +} + +class Unzipper { + var dc0 = Repeater("-") + fun unzip(chars: Array>): String { + val sb = java.lang.StringBuilder() + for (pr in chars) { + sb.append(dc0.repeat(pr.value.toString(), pr.key!!)) + } + return sb.toString() + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/TestBenchmarkClasses.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/TestBenchmarkClasses.kt new file mode 100644 index 0000000000..f1692a0334 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/benchmark/TestBenchmarkClasses.kt @@ -0,0 +1,49 @@ +package org.utbot.examples.benchmark + +import org.utbot.examples.samples.benchmark.Fibonacci +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.InvokeInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import java.math.BigInteger +import javafx.util.Pair +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class TestBenchmarkClasses { + lateinit var utContext: AutoCloseable + + @Test + fun testRepeater() { + ConcreteExecutor( + CoverageInstrumentation, + Repeater::class.java.protectionDomain.codeSource.location.path + ).use { + val dc0 = Repeater(", ") + val res0 = it.execute(Repeater::concat, arrayOf(dc0, "flex", "mega-", 2)) + assertEquals("mega-mega-flex", res0.getOrNull()) + + + val dc1 = Unzipper() + val arr = arrayOf(Pair(1, 'h'), Pair(1, 'e'), Pair(2, 'l'), Pair(1, 'o')) + val res1 = it.execute(Unzipper::unzip, arrayOf(dc1, arr)) + assertEquals("h-e-ll-o-", res1.getOrNull()) + } + } + + @Test + fun testFibonacci() { + ConcreteExecutor( + InvokeInstrumentation(), + Fibonacci::class.java.protectionDomain.codeSource.location.path + ).use { + val res = + it.execute( + Fibonacci::calc, + arrayOf(1, 1, 10) + ) + assertEquals(Result.success(BigInteger.valueOf(89)), res) + } + } +} + diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestMixedExTrace.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestMixedExTrace.kt new file mode 100644 index 0000000000..560a314633 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestMixedExTrace.kt @@ -0,0 +1,54 @@ +package org.utbot.examples.et + +import org.utbot.examples.samples.et.ClassMixedWithNotInstrumented_Instr +import org.utbot.examples.samples.et.ClassMixedWithNotInstrumented_Not_Instr +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.et.ExecutionTraceInstrumentation +import org.utbot.instrumentation.instrumentation.et.convert +import org.utbot.instrumentation.instrumentation.et.function +import org.utbot.instrumentation.instrumentation.et.invoke +import org.utbot.instrumentation.instrumentation.et.pass +import org.utbot.instrumentation.instrumentation.et.ret +import org.utbot.instrumentation.util.Isolated +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class TestMixedExTrace { + lateinit var utContext: AutoCloseable + + + val CLASSPATH = ClassMixedWithNotInstrumented_Instr::class.java.protectionDomain.codeSource.location.path + + @Disabled("The execution trace of mixed calls is not properly supported yet") + // `mixed calls` means such calls: A -> {B -> {A -> ...}, ... }, where A has been instrumented but B has not. + @Test + fun testMixedDoesNotThrow() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassMixedWithNotInstrumented_Instr::a, it) + val B = Isolated(ClassMixedWithNotInstrumented_Not_Instr::b, it) + + val res = A(1) + assertEquals( + function(A.signature) { + pass() + invoke(B.signature) { // TODO: think on clear DSL API for not instrumented calls + invoke(A.signature) { + pass() + ret() + } + invoke(A.signature) { + pass() + ret() + } + } + ret() + }, + convert(res) + ) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestSimpleExTrace.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestSimpleExTrace.kt new file mode 100644 index 0000000000..e7dab32d5f --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestSimpleExTrace.kt @@ -0,0 +1,525 @@ +package org.utbot.examples.et + +import org.utbot.examples.samples.et.ClassBinaryRecursionWithThrow +import org.utbot.examples.samples.et.ClassBinaryRecursionWithTrickyThrow +import org.utbot.examples.samples.et.ClassSimple +import org.utbot.examples.samples.et.ClassSimpleCatch +import org.utbot.examples.samples.et.ClassSimpleNPE +import org.utbot.examples.samples.et.ClassSimpleRecursive +import org.utbot.instrumentation.ConcreteExecutor +import org.utbot.instrumentation.instrumentation.et.ExecutionTraceInstrumentation +import org.utbot.instrumentation.instrumentation.et.convert +import org.utbot.instrumentation.instrumentation.et.explThr +import org.utbot.instrumentation.instrumentation.et.function +import org.utbot.instrumentation.instrumentation.et.implThr +import org.utbot.instrumentation.instrumentation.et.invoke +import org.utbot.instrumentation.instrumentation.et.pass +import org.utbot.instrumentation.instrumentation.et.ret +import org.utbot.instrumentation.util.Isolated +import kotlin.reflect.full.declaredFunctions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + + +class TestSimpleExTrace { + lateinit var utContext: AutoCloseable + + val CLASSPATH = ClassSimple::class.java.protectionDomain.codeSource.location.path + + /** + * #1. doesNotThrow + * #2. alwaysThrows + * #3. maybeThrows(-1) maybeThrows(0) + */ + @Test + fun testClassSimple() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val alwaysThrows = Isolated(ClassSimple::alwaysThrows, it) + val maybeThrows = Isolated(ClassSimple::maybeThrows, it) + val doesNotThrow = Isolated(ClassSimple::doesNotThrow, it) + + val et1 = doesNotThrow() + assertEquals( + function(doesNotThrow.signature) { + pass() + ret() + }, + convert(et1) + ) + + val et2 = alwaysThrows() + assertEquals( + function(alwaysThrows.signature) { + pass() + explThr() + }, + convert(et2) + ) + + val et3 = maybeThrows(-1) + assertEquals( + function(maybeThrows.signature) { + pass() + explThr() + }, + convert(et3) + ) + + val et4 = maybeThrows(0) + assertEquals( + function(maybeThrows.signature) { + pass() + ret() + }, + convert(et4) + ) + } + } + + + /** + * #1. A + * #2. A_catches + * #3. A_doesNotCatch + * #4. A_catchesWrongException + */ + @Test + fun testClasSimpleCatch() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassSimpleCatch::A, it) + val A_catches = Isolated(ClassSimpleCatch::A_catches, it) + val A_catchesWrongException = Isolated(ClassSimpleCatch::A_catchesWrongException, it) + val A_doesNotCatch = Isolated(ClassSimpleCatch::A_doesNotCatch, it) + + val B = Isolated(ClassSimpleCatch::B, it) + val B_throws = Isolated(ClassSimpleCatch::B_throws, it) + + + val et1 = A() + assertEquals( + function(A.signature) { + invoke(B.signature) { + pass() + ret() + } + pass() + ret() + }, + convert(et1) + ) + + val et2 = A_catches() + assertEquals( + function(A_catches.signature) { + invoke(B_throws.signature) { + pass() + implThr() + } + pass() + ret() + }, + convert(et2) + ) + + val et3 = A_catchesWrongException() + assertEquals( + function(A_catchesWrongException.signature) { + invoke(B_throws.signature) { + pass() + implThr() + } + }, + convert(et3) + ) + + val et4 = A_doesNotCatch() + assertEquals( + function(A_doesNotCatch.signature) { + invoke(B_throws.signature) { + pass() + implThr() + } + }, + convert(et4) + ) + } + } + + /** + * #1. A(3) + * #2. A_recursive(3, 2) + * #3. A_recursive(3, 1) + */ + @Test + fun testClassSimpleRecursive() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassSimpleRecursive::A, it) + val A_recursive = Isolated(ClassSimpleRecursive::A_recursive, it) + + val et1 = A(3) + assertEquals( + function(A.signature) { + pass() + invoke(A.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + ret() + } + ret() + }, + convert(et1) + ) + + val et2 = A_recursive(3, 2) + assertEquals( + function(A_recursive.signature) { + pass() + invoke(A_recursive.signature) { + pass() + invoke(A_recursive.signature) { + pass() + explThr() + } + pass() + explThr() + } + pass() + ret() + }, + convert(et2) + ) + + val et3 = A_recursive(3, 1) + assertEquals( + function(A_recursive.signature) { + pass() + invoke(A_recursive.signature) { + pass() + invoke(A_recursive.signature) { + pass() + explThr() + } + pass() + explThr() + } + }, + convert(et3) + ) + } + } + + /** + * #1. A(1, 10, 2) + * #2. A_catchesAll(true, 2) + * #3. A_notAll(right, 2) + */ + @Test + fun testClassBinaryRecursionWithTrickyThrow() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassBinaryRecursionWithTrickyThrow::A, it) + val A_catchesAll = Isolated(ClassBinaryRecursionWithTrickyThrow::A_catchesAll, it) + val A_notAll = Isolated(ClassBinaryRecursionWithTrickyThrow::A_notAll, it) + + val et1 = A(1, 10, 2) + assertEquals( + function(A.signature) { + pass() + invoke(A.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + ret() + }, + convert(et1) + ) + + val et2 = A_catchesAll(true, 2) + + assertEquals( + function(A_catchesAll.signature) { + pass() + invoke(A_catchesAll.signature) { + pass() + invoke(A_catchesAll.signature) { + pass() + explThr() + } + pass() + invoke(A_catchesAll.signature) { + pass() + explThr() + } + pass() + ret() + } + pass() + invoke(A_catchesAll.signature) { + pass() + invoke(A_catchesAll.signature) { + pass() + explThr() + } + } + pass() + ret() + }, + convert(et2) + ) + + val et3 = A_notAll(false, 2) + assertEquals( + function(A_notAll.signature) { + pass() + invoke(A_notAll.signature) { + pass() + invoke(A_notAll.signature) { + pass() + explThr() + } + } + pass() + invoke(A_notAll.signature) { + pass() + invoke(A_notAll.signature) { + pass() + explThr() + } + pass() + invoke(A_notAll.signature) { + pass() + explThr() + } + } + pass() + ret() + }, + convert(et3) + ) + } + } + + + /** + * #1. A(1, 2, false) + * #2. A(1, 2, true) + */ + @Test + fun testClassBinaryRecursionWithThrow() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassBinaryRecursionWithThrow::A, it) + val B = Isolated(ClassBinaryRecursionWithThrow::class.declaredFunctions.first { it.name == "B" }, it) + + val et1 = A(1, 2, false) + assertEquals( + function(A.signature) { + pass() + invoke(B.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(B.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + ret() + } + ret() + } + ret() + }, + convert(et1) + ) + + val et2 = A(1, 2, true) + assertEquals( + function(A.signature) { + pass() + invoke(B.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(B.signature) { + pass() + explThr() + } + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + invoke(A.signature) { + pass() + ret() + } + pass() + invoke(A.signature) { + pass() + ret() + } + ret() + } + ret() + }, + convert(et2) + ) + } + } + + /** + * #1. A(false) + * #2. A(true) + */ + @Test + fun testClassSimpleNPE() { + ConcreteExecutor( + ExecutionTraceInstrumentation(), + CLASSPATH + ).use { + val A = Isolated(ClassSimpleNPE::A, it) + val B = Isolated(ClassSimpleNPE::B, it) + val C = Isolated(ClassSimpleNPE::C, it) + val D = Isolated(ClassSimpleNPE::D, it) + + val thisObject = ClassSimpleNPE() + + val et1 = A(thisObject, false) + assertEquals( + function(A.signature) { + pass() + invoke(B.signature) { + pass() + invoke(B.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(C.signature) { + pass() + invoke(C.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(D.signature) { + pass() + invoke(D.signature) { + pass() + invoke(D.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + ret() + } + ret() + }, + convert(et1) + ) + + val et2 = A(thisObject, true) + assertEquals( + function(A.signature) { + pass() + invoke(B.signature) { + pass() + invoke(B.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(C.signature) { + pass() + invoke(C.signature) { + pass() + ret() + } + pass() + ret() + } + pass() + invoke(D.signature) { + pass() + implThr() + } + }, + convert(et2) + ) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestStaticsUsage.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestStaticsUsage.kt new file mode 100644 index 0000000000..a7ec3e5780 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/et/TestStaticsUsage.kt @@ -0,0 +1,58 @@ +package org.utbot.examples.et + +import org.utbot.examples.objects.ObjectWithStaticFieldsClass +import org.utbot.examples.objects.ObjectWithStaticFieldsExample +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.field +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.et.ExecutionTraceInstrumentation +import org.utbot.instrumentation.withInstrumentation +import kotlin.reflect.jvm.javaField +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class StaticsUsageDetectionTest { + lateinit var utContext: AutoCloseable + + @BeforeEach + fun initContext() { + utContext = UtContext.setUtContext(UtContext(ClassLoader.getSystemClassLoader())) + } + + @AfterEach + fun closeConctext() { + utContext.close() + } + + + @Test + fun testStaticsUsageOneUsage() { + withInstrumentation( + ExecutionTraceInstrumentation(), + ObjectWithStaticFieldsExample::class.java.protectionDomain.codeSource.location.path + ) { + val instance = ObjectWithStaticFieldsExample() + val classInstance = ObjectWithStaticFieldsClass() + classInstance.x = 200 + classInstance.y = 200 + val result = it.execute(ObjectWithStaticFieldsExample::setStaticField, arrayOf(instance, classInstance)) + assertEquals(ObjectWithStaticFieldsClass::staticValue.javaField, result.usedStatics.single().field) + } + } + + @Test + fun testStaticsUsageZeroUsages() { + withInstrumentation( + ExecutionTraceInstrumentation(), + ObjectWithStaticFieldsExample::class.java.protectionDomain.codeSource.location.path + ) { + val instance = ObjectWithStaticFieldsExample() + val classInstance = ObjectWithStaticFieldsClass() + val result = it.execute(ObjectWithStaticFieldsExample::setStaticField, arrayOf(instance, classInstance)) + assertTrue(result.usedStatics.isEmpty()) + } + } +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/jacoco/TestSameAsJaCoCo.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/jacoco/TestSameAsJaCoCo.kt new file mode 100644 index 0000000000..de8051a775 --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/jacoco/TestSameAsJaCoCo.kt @@ -0,0 +1,214 @@ +package org.utbot.examples.jacoco + +import org.utbot.common.Reflection +import org.utbot.examples.samples.jacoco.ExceptionExamples +import org.utbot.examples.samples.jacoco.MonitorUsage +import org.utbot.examples.samples.jacoco.Recursion +import org.utbot.framework.plugin.api.util.signature +import org.utbot.instrumentation.execute +import org.utbot.instrumentation.instrumentation.ArgumentList +import org.utbot.instrumentation.instrumentation.coverage.CoverageInfo +import org.utbot.instrumentation.instrumentation.coverage.CoverageInstrumentation +import org.utbot.instrumentation.instrumentation.coverage.collectCoverage +import org.utbot.instrumentation.withInstrumentation +import java.io.InputStream +import java.lang.reflect.InvocationTargetException +import kotlin.reflect.KCallable +import kotlin.reflect.KClass +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.instanceParameter +import org.jacoco.core.analysis.Analyzer +import org.jacoco.core.analysis.CoverageBuilder +import org.jacoco.core.analysis.IClassCoverage +import org.jacoco.core.data.ExecutionDataStore +import org.jacoco.core.data.SessionInfoStore +import org.jacoco.core.instr.Instrumenter +import org.jacoco.core.runtime.IRuntime +import org.jacoco.core.runtime.LoggerRuntime +import org.jacoco.core.runtime.RuntimeData +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test + +class TestSameAsJaCoCo { + private fun checkSame(kClass: KClass<*>, method: KCallable<*>, executions: List) { + val methodCoverageJaCoCo = methodCoverageWithJaCoCo(kClass, method, executions) + val methodCoverageOur = methodCoverage(kClass, method, executions) + + Assertions.assertTrue(methodCoverageJaCoCo.first <= methodCoverageOur.first) { + "JaCoCo (${methodCoverageJaCoCo.first} should not cover more instructions than we cover" + + "(${methodCoverageOur.first})" + } + Assertions.assertEquals(methodCoverageJaCoCo, methodCoverageOur) + } + + @Test + @Disabled( + "Synchronized causes the creation of extra bytecode instructions by java compiler." + + "JaCoCo ignores them, but we do not." + ) + fun testSimpleMonitor() { + val monitorUsage = MonitorUsage() + val executions = listOf(listOf(monitorUsage, 1), listOf(monitorUsage, -1), listOf(monitorUsage, 0)) + + checkSame(MonitorUsage::class, MonitorUsage::simpleMonitor, executions) + } + + @Test + @Disabled( + "Finally block is copied into each catch branch, so instructions are duplicated." + + "JaCoCo treats such instructions as one instruction, but we treat them as separate." + ) + fun testFinallyChanging() { + val exceptionExamples = ExceptionExamples() + val executions = listOf(listOf(exceptionExamples, 0)) + + checkSame(ExceptionExamples::class, ExceptionExamples::finallyChanging, executions) + } + + @Test + @Disabled("If an exception happens, JaCoCo ignores passed instructions before the exception, but we do not.") + fun testThrowException() { + val exceptionExamples = ExceptionExamples() + val executions = listOf(listOf(exceptionExamples, -1), listOf(exceptionExamples, 1)) + + checkSame(ExceptionExamples::class, ExceptionExamples::throwException, executions) + } + + + @Test + @Disabled("For some reason (?), JaCoCo sometimes ignores instructions.") + fun recursionWithExceptionTest() { + val recursion = Recursion() + val executions = listOf(listOf(recursion, 41), listOf(recursion, 42), listOf(recursion, 43)) + + checkSame(Recursion::class, Recursion::recursionWithException, executions) + } + + @Test + @Disabled("For some reason (?), JaCoCo doesn't count instructions, even they were executed successfully.") + fun infiniteRecursionTest() { + val recursion = Recursion() + val executions = listOf(listOf(recursion, 0)) + + checkSame(Recursion::class, Recursion::infiniteRecursion, executions) + } +} + +private fun methodCoverageWithJaCoCo(kClass: KClass<*>, method: KCallable<*>, executions: List): Pair { + val methodSignature = method.signature + val coverage = calculateCoverage(kClass) { clazz -> + val instrumentedMethod = clazz.declaredFunctions.single { it.signature == methodSignature } + val onInstance = instrumentedMethod.instanceParameter != null + for (execution in executions) { + try { + if (onInstance) { + instrumentedMethod.call(clazz.java.anyInstance, *execution.drop(1).toTypedArray()) + } else { + instrumentedMethod.call(*execution.toTypedArray()) + } + } catch (_: InvocationTargetException) { + } + } + } + val methodCoverage = coverage.classes + .single { it.qualifiedName == kClass.qualifiedName } + .methods + .single { + "${it.name}${it.desc}" == methodSignature + } + + return methodCoverage.instructionCounter.let { it.coveredCount to it.coveredCount } +} + +private fun methodCoverage(kClass: KClass<*>, method: KCallable<*>, executions: List): Pair { + return withInstrumentation( + CoverageInstrumentation, + kClass.java.protectionDomain.codeSource.location.path + ) { executor -> + for (execution in executions) { + executor.execute(method, execution.toTypedArray()) + } + + val methodSignature = method.signature + val coverage = executor.collectCoverage(kClass.java) + coverage.toMethodCoverage(methodSignature) + } +} + +private fun CoverageInfo.toMethodCoverage(methodSignature: String): Pair { + val methodRange = methodToInstrRange[methodSignature]!! + val visitedCount = visitedInstrs.filter { it in methodRange }.size + return visitedCount to methodRange.count() +} + +// The following helper functions for counting JaCoCo coverage were copied from `utbot-framework`. + +private fun calculateCoverage(clazz: KClass<*>, block: (KClass<*>) -> Unit): CoverageBuilder { + val targetName = clazz.qualifiedName!! + + // IRuntime instance to collect execution data + val runtime: IRuntime = LoggerRuntime() + + // create a modified version of target class with probes + val instrumenter = Instrumenter(runtime) + val instrumented = instrument(clazz, instrumenter) + + // startup the runtime + val data = RuntimeData() + runtime.startup(data) + + // load class from byte[] instances + val memoryClassLoader = MemoryClassLoader() + memoryClassLoader.addDefinition(targetName, instrumented) + val targetClass = memoryClassLoader.loadClass(targetName).kotlin + + // execute code + block(targetClass) + + // shutdown the runtime + val executionData = ExecutionDataStore() + val sessionInfos = SessionInfoStore() + data.collect(executionData, sessionInfos, false) + runtime.shutdown() + + // calculate coverage + return CoverageBuilder().apply { + val analyzer = Analyzer(executionData, this) + clazz.asInputStream().use { + analyzer.analyzeClass(it, targetName) + } + } +} + +private fun instrument(clazz: KClass<*>, instrumenter: Instrumenter): ByteArray = + clazz.asInputStream().use { + instrumenter.instrument(it, clazz.qualifiedName) + } + +private fun KClass<*>.asInputStream(): InputStream = + java.getResourceAsStream("/${qualifiedName!!.replace('.', '/')}.class")!! + +private class MemoryClassLoader : ClassLoader() { + private val definitions: MutableMap = mutableMapOf() + + fun addDefinition(name: String, bytes: ByteArray) { + definitions[name] = bytes + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + val bytes = definitions[name] + return if (bytes != null) { + defineClass(name, bytes, 0, bytes.size) + } else super.loadClass(name, resolve) + } +} + +private val IClassCoverage.qualifiedName: String + get() = this.name.replace('/', '.') + +@Suppress("DEPRECATION") +private val Class<*>.anyInstance: Any + get() { + return Reflection.unsafe.allocateInstance(this) + } \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ClassWithWrongPackage.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ClassWithWrongPackage.kt new file mode 100644 index 0000000000..62638aa43a --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ClassWithWrongPackage.kt @@ -0,0 +1,4 @@ +package org.utbot.examples.samples.wrongpackage + +class ClassWithWrongPackage { +} \ No newline at end of file diff --git a/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ExampleClass.kt b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ExampleClass.kt new file mode 100644 index 0000000000..6f84b7c62c --- /dev/null +++ b/utbot-instrumentation-tests/src/test/kotlin/org/utbot/examples/samples/ExampleClass.kt @@ -0,0 +1,67 @@ +package org.utbot.examples.samples + +class ExampleClass { + var x1 = 1 + val arr = BooleanArray(5) + val arr2 = BooleanArray(10) + + fun bar(x: Int) { + if (x > 1) { + x1++ + x1++ + } else { + x1-- + x1-- + } + } + + fun kek2(x: Int) { + arr[x] = true + } + + fun foo(x: Int): Int { + x1 = x xor 2 + + var was = false + + for (i in 0 until x) { + was = true + var x2 = 0 + if (i > 5) { + was = false + x2 = 1 + } + if (was && x2 == 0) { + was = true + } + } + + // empty lines + return if (was) x1 else x1 + 1 + } + + fun dependsOnField() { + x1 = x1 xor 1 + if (x1 and 1 == 1) { + x1 += 4 + } else { + x1 += 2 + } + } + + fun dependsOnFieldReturn(): Int { + x1 = x1 xor 1 + if (x1 and 1 == 1) { + x1 += 4 + } else { + x1 += 2 + } + return x1 + } + + fun emptyMethod() { + } + + @Suppress("unused") + fun use() = arr2.size +} \ No newline at end of file diff --git a/utbot-instrumentation/build.gradle b/utbot-instrumentation/build.gradle index cf64580cb1..a1ee9dfc96 100644 --- a/utbot-instrumentation/build.gradle +++ b/utbot-instrumentation/build.gradle @@ -10,7 +10,6 @@ dependencies { implementation group: 'de.javakaffee', name: 'kryo-serializers', version: kryo_serializers_version implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version - // TODO: this is necessary for inline classes mocking in UtExecutionInstrumentation implementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' implementation group: 'org.mockito', name: 'mockito-inline', version: '4.2.0' @@ -32,4 +31,4 @@ configurations { artifacts { instrumentationArchive jar -} \ No newline at end of file +} diff --git a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt index c0730862dd..9b46a8d5cf 100644 --- a/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt +++ b/utbot-instrumentation/src/main/kotlin/org/utbot/instrumentation/process/ChildProcessRunner.kt @@ -1,5 +1,6 @@ package org.utbot.instrumentation.process +import mu.KotlinLogging import org.utbot.common.bracket import org.utbot.common.debug import org.utbot.common.firstOrNullResourceIS @@ -9,11 +10,11 @@ import org.utbot.common.pid import org.utbot.common.scanForResourcesContaining import org.utbot.common.utBotTempDirectory import org.utbot.framework.JdkPathService +import org.utbot.framework.UtSettings import org.utbot.instrumentation.Settings import org.utbot.instrumentation.agent.DynamicClassTransformer import java.io.File import java.nio.file.Paths -import mu.KotlinLogging private val logger = KotlinLogging.logger {} private var processSeqN = 0 @@ -30,29 +31,44 @@ class ChildProcessRunner { debugCmd + listOf("-javaagent:$jarFile", "-ea", "-jar", "$jarFile") } - lateinit var errorLogFile: File + var errorLogFile: File = NULL_FILE fun start(): Process { logger.debug { "Starting child process: ${cmds.joinToString(" ")}" } processSeqN++ - val dir = File(utBotTempDirectory.toFile(), ERRORS_FILE_PREFIX) - dir.mkdirs() - errorLogFile = File(dir, "${hashCode()}-${processSeqN}.log") + if (UtSettings.logConcreteExecutionErrors) { + UT_BOT_TEMP_DIR.mkdirs() + errorLogFile = File(UT_BOT_TEMP_DIR, "${hashCode()}-${processSeqN}.log") + } val processBuilder = ProcessBuilder(cmds).redirectError(errorLogFile) return processBuilder.start().also { - logger.debug { "Process started with PID=${it.pid} , error log: ${errorLogFile.absolutePath}" } + logger.debug { "Process started with PID=${it.pid}" } + + if (UtSettings.logConcreteExecutionErrors) { + logger.debug { "Child process error log: ${errorLogFile.absolutePath}" } + } } } companion object { private const val UTBOT_INSTRUMENTATION = "utbot-instrumentation" private const val ERRORS_FILE_PREFIX = "utbot-childprocess-errors" - private const val INSTRUMENTATION_LIB = "instrumentation-lib" + private const val INSTRUMENTATION_LIB = "lib" private const val DEBUG_RUN_CMD = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,quiet=y,address=5005" + private val UT_BOT_TEMP_DIR: File = File(utBotTempDirectory.toFile(), ERRORS_FILE_PREFIX) + + private val NULL_FILE_PATH: String = if (System.getProperty("os.name").startsWith("Windows")) { + "NUL" + } else { + "/dev/null" + } + + private val NULL_FILE = File(NULL_FILE_PATH) + /** * * Firstly, searches for utbot-instrumentation jar in the classpath. * diff --git a/utbot-intellij/build.gradle b/utbot-intellij/build.gradle index 649f2c0b49..6859479ac7 100644 --- a/utbot-intellij/build.gradle +++ b/utbot-intellij/build.gradle @@ -20,8 +20,6 @@ buildscript { } dependencies { - api project(':utbot-summary') - api project(':utbot-analytics') api ('com.esotericsoftware:kryo:5.1.1') implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerator.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerator.kt index 4c4ef7befe..e8472f9112 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerator.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/CodeGenerator.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.project.Project import com.intellij.psi.PsiMethod import com.intellij.refactoring.util.classMembers.MemberInfo +import org.utbot.engine.UtBotSymbolicEngine import java.nio.file.Path import java.nio.file.Paths import kotlin.reflect.KClass @@ -32,14 +33,15 @@ class CodeGenerator( buildDir: String, classpath: String, pluginJarsPath: String, - isCanceled: () -> Boolean + configureEngine: (UtBotSymbolicEngine) -> Unit = {}, + isCanceled: () -> Boolean, ) { init { UtSettings.testMinimizationStrategyType = TestSelectionStrategyType.COVERAGE_STRATEGY } - private val generator = project.service().testCasesGenerator.apply { - init(Paths.get(buildDir), classpath, pluginJarsPath, isCanceled) + val generator = (project.service().testCasesGenerator as UtBotTestCaseGenerator).apply { + init(Paths.get(buildDir), classpath, pluginJarsPath, configureEngine, isCanceled) } private val settingsState = project.service().state @@ -49,7 +51,7 @@ class CodeGenerator( fun generateForSeveralMethods(methods: List>, timeout:Long = UtSettings.utBotGenerationTimeoutInMillis): List { logger.info("Tests generating parameters $settingsState") - return (generator as UtBotTestCaseGenerator) + return generator .generateForSeveralMethods(methods, mockStrategy, chosenClassesToMockAlways, methodsGenerationTimeout = timeout) .map { it.summarize(searchDirectory) } } @@ -57,7 +59,10 @@ class CodeGenerator( fun findMethodsInClassMatchingSelected(clazz: KClass<*>, selectedMethods: List): List> { val selectedSignatures = selectedMethods.map { it.signature() } - return clazz.functions.filter { it.signature().normalized() in selectedSignatures }.map { UtMethod(it, clazz) } + return clazz.functions + .sortedWith(compareBy { selectedSignatures.indexOf(it.signature()) }) + .filter { it.signature().normalized() in selectedSignatures } + .map { UtMethod(it, clazz) } } fun findMethodParams(clazz: KClass<*>, methods: List): Map, List> { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/TestGenerator.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/TestGenerator.kt index 581c27f24c..b27bd8d365 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/TestGenerator.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/generator/TestGenerator.kt @@ -15,10 +15,6 @@ import org.utbot.framework.plugin.api.UtTestCase import org.utbot.intellij.plugin.sarif.SarifReportIdea import org.utbot.intellij.plugin.sarif.SourceFindingStrategyIdea import org.utbot.intellij.plugin.settings.Settings -import org.utbot.intellij.plugin.ui.GenerateTestsModel -import org.utbot.intellij.plugin.ui.SarifReportNotifier -import org.utbot.intellij.plugin.ui.TestsReportNotifier -import org.utbot.intellij.plugin.ui.packageName import org.utbot.intellij.plugin.ui.utils.getOrCreateSarifReportsPath import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath import org.utbot.sarif.SarifReport @@ -27,34 +23,37 @@ import com.intellij.codeInsight.FileModificationService import com.intellij.ide.fileTemplates.FileTemplateManager import com.intellij.ide.fileTemplates.FileTemplateUtil import com.intellij.ide.fileTemplates.JavaTemplateUtil +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction import com.intellij.openapi.command.executeCommand import com.intellij.openapi.components.service import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.DumbService import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Computable import com.intellij.openapi.vfs.VfsUtil -import com.intellij.psi.JavaDirectoryService -import com.intellij.psi.PsiClass -import com.intellij.psi.PsiClassOwner -import com.intellij.psi.PsiDirectory -import com.intellij.psi.PsiDocumentManager -import com.intellij.psi.PsiElement -import com.intellij.psi.PsiFile -import com.intellij.psi.PsiMethod +import com.intellij.psi.* import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.psi.codeStyle.JavaCodeStyleManager import com.intellij.psi.search.GlobalSearchScopesCore import com.intellij.testIntegration.TestIntegrationUtils import com.intellij.util.IncorrectOperationException +import com.intellij.util.concurrency.AppExecutorUtil import com.intellij.util.io.exists import com.siyeh.ig.psiutils.ImportUtils +import java.nio.file.Path import java.nio.file.Paths +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit import org.jetbrains.kotlin.asJava.classes.KtUltraLightClass import org.jetbrains.kotlin.idea.core.ShortenReferences import org.jetbrains.kotlin.idea.core.getPackage import org.jetbrains.kotlin.idea.core.util.toPsiDirectory import org.jetbrains.kotlin.idea.util.ImportInsertHelperImpl +import org.jetbrains.kotlin.idea.util.application.invokeLater import org.jetbrains.kotlin.name.FqName import org.jetbrains.kotlin.psi.KtClass import org.jetbrains.kotlin.psi.KtNamedFunction @@ -62,19 +61,39 @@ import org.jetbrains.kotlin.psi.KtPsiFactory import org.jetbrains.kotlin.psi.psiUtil.endOffset import org.jetbrains.kotlin.psi.psiUtil.startOffset import org.jetbrains.kotlin.scripting.resolve.classId +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext import org.utbot.intellij.plugin.error.showErrorDialogLater +import org.utbot.intellij.plugin.generator.TestGenerator.Target.* +import org.utbot.intellij.plugin.ui.GenerateTestsModel +import org.utbot.intellij.plugin.ui.SarifReportNotifier +import org.utbot.intellij.plugin.ui.TestReportUrlOpeningListener +import org.utbot.intellij.plugin.ui.TestsReportNotifier +import org.utbot.intellij.plugin.ui.packageName object TestGenerator { - fun generateTests(model: GenerateTestsModel, testCases: Map>) { - runWriteCommandAction(model.project, "Generate tests with UtBot", null, { - generateTestsInternal(model, testCases) - }) + private enum class Target {THREAD_POOL, READ_ACTION, WRITE_ACTION, EDT_LATER} + + private fun run(target: Target, runnable: Runnable) { + UtContext.currentContext()?.let { + when (target) { + THREAD_POOL -> AppExecutorUtil.getAppExecutorService().submit { + withUtContext(it) { + runnable.run() + } + } + READ_ACTION -> runReadAction { withUtContext(it) { runnable.run() } } + WRITE_ACTION -> runWriteAction { withUtContext(it) { runnable.run() } } + EDT_LATER -> invokeLater { withUtContext(it) { runnable.run() } } + } + } ?: error("No context in thread ${Thread.currentThread()}") } - private fun generateTestsInternal(model: GenerateTestsModel, testCasesByClass: Map>) { + fun generateTests(model: GenerateTestsModel, testCasesByClass: Map>) { val baseTestDirectory = model.testSourceRoot?.toPsiDirectory(model.project) ?: return val allTestPackages = getPackageDirectories(baseTestDirectory) + val latch = CountDownLatch(testCasesByClass.size) for (srcClass in testCasesByClass.keys) { val testCases = testCasesByClass[srcClass] ?: continue @@ -82,20 +101,39 @@ object TestGenerator { val classPackageName = if (model.testPackageName.isNullOrEmpty()) srcClass.containingFile.containingDirectory.getPackage()?.qualifiedName else model.testPackageName val testDirectory = allTestPackages[classPackageName] ?: baseTestDirectory - val testClass = createTestClass(srcClass, testDirectory, model.codegenLanguage) ?: continue + val testClass = createTestClass(srcClass, testDirectory, model) ?: continue val file = testClass.containingFile - - addTestMethodsAndSaveReports(testClass, file, testCases, model) + runWriteCommandAction(model.project, "Generate tests with UtBot", null, { + try { + addTestMethodsAndSaveReports(testClass, file, testCases, model, latch) + } catch (e: IncorrectOperationException) { + showCreatingClassError(model.project, createTestClassName(srcClass)) + } + }) } catch (e: IncorrectOperationException) { showCreatingClassError(model.project, createTestClassName(srcClass)) } } + run(READ_ACTION) { + val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) + run(THREAD_POOL) { + waitForCountDown(latch, model, sarifReportsPath) + } + } + } - mergeSarifReports(model) + private fun waitForCountDown(latch: CountDownLatch, model: GenerateTestsModel, sarifReportsPath : Path) { + try { + if (!latch.await(5, TimeUnit.SECONDS)) { + run(THREAD_POOL) { waitForCountDown(latch, model, sarifReportsPath) } + } else { + mergeSarifReports(model, sarifReportsPath) + } + } catch (ignored: InterruptedException) { + } } - private fun mergeSarifReports(model: GenerateTestsModel) { - val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath() + private fun mergeSarifReports(model: GenerateTestsModel, sarifReportsPath : Path) { val sarifReports = sarifReportsPath.toFile() .walkTopDown() .filter { it.extension == "sarif" } @@ -103,7 +141,7 @@ object TestGenerator { .toList() val mergedReport = SarifReport.mergeReports(sarifReports) - val mergedReportPath = sarifReportsPath.resolve("${model.project.name}-utbot-merged.sarif") + val mergedReportPath = sarifReportsPath.resolve("${model.project.name}Report.sarif") mergedReportPath.toFile().writeText(mergedReport) // notifying the user @@ -132,34 +170,44 @@ object TestGenerator { } } - private fun createTestClass(srcClass: PsiClass, testDirectory: PsiDirectory, codegenLanguage: CodegenLanguage): PsiClass? { + private fun createTestClass(srcClass: PsiClass, testDirectory: PsiDirectory, model: GenerateTestsModel): PsiClass? { val testClassName = createTestClassName(srcClass) val aPackage = JavaDirectoryService.getInstance().getPackage(testDirectory) if (aPackage != null) { val scope = GlobalSearchScopesCore.directoryScope(testDirectory, false) - // Here we use firstOrNull(), because by some unknown reason - // findClassByShortName() may return two identical objects. - // Be careful, do not use singleOrNull() here, because it expects - // the array to contain strictly one element and otherwise returns null. - aPackage.findClassByShortName(testClassName, scope) - .firstOrNull { - when (codegenLanguage) { - CodegenLanguage.JAVA -> it !is KtUltraLightClass - CodegenLanguage.KOTLIN -> it is KtUltraLightClass - } - }?.let { - return if (FileModificationService.getInstance().preparePsiElementForWrite(it)) it else null + val application = ApplicationManager.getApplication() + val testClass = application.executeOnPooledThread { + return@executeOnPooledThread application.runReadAction { + DumbService.getInstance(model.project).runReadActionInSmartMode(Computable { + // Here we use firstOrNull(), because by some unknown reason + // findClassByShortName() may return two identical objects. + // Be careful, do not use singleOrNull() here, because it expects + // the array to contain strictly one element and otherwise returns null. + return@Computable aPackage.findClassByShortName(testClassName, scope) + .firstOrNull { + when (model.codegenLanguage) { + CodegenLanguage.JAVA -> it !is KtUltraLightClass + CodegenLanguage.KOTLIN -> it is KtUltraLightClass + } + } + }) } + }.get() + + testClass?.let { + return if (FileModificationService.getInstance().preparePsiElementForWrite(it)) it else null + } } val fileTemplate = FileTemplateManager.getInstance(testDirectory.project).getInternalTemplate( - when (codegenLanguage) { + when (model.codegenLanguage) { CodegenLanguage.JAVA -> JavaTemplateUtil.INTERNAL_CLASS_TEMPLATE_NAME CodegenLanguage.KOTLIN -> "Kotlin Class" } ) + runWriteAction { testDirectory.findFile(testClassName + model.codegenLanguage.extension)?.delete() } val createFromTemplate: PsiElement = FileTemplateUtil.createFromTemplate( fileTemplate, testClassName, @@ -175,6 +223,7 @@ object TestGenerator { file: PsiFile, testCases: List, model: GenerateTestsModel, + reportsCountDown: CountDownLatch, ) { val selectedMethods = TestIntegrationUtils.extractClassMethods(testClass, false) val testFramework = model.testFramework @@ -205,33 +254,50 @@ object TestGenerator { when (generator) { is ModelBasedTestCodeGenerator -> { val editor = CodeInsightUtil.positionCursorAtLBrace(testClass.project, file, testClass) - val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases) - val generatedTestsCode = testsCodeWithTestReport.generatedCode + //TODO Use PsiDocumentManager.getInstance(model.project).getDocument(file) + // if we don't want to open _all_ new files with tests in editor one-by-one + run(THREAD_POOL) { + val testsCodeWithTestReport = generator.generateAsStringWithTestReport(testCases) + val generatedTestsCode = testsCodeWithTestReport.generatedCode + run(EDT_LATER) { + run(WRITE_ACTION) { + unblockDocument(testClass.project, editor.document) + // TODO: JIRA:1246 - display warnings if we rewrite the file + executeCommand(testClass.project, "Insert Generated Tests") { + editor.document.setText(generatedTestsCode) + } + unblockDocument(testClass.project, editor.document) + + // after committing the document the `testClass` is invalid in PsiTree, + // so we have to reload it from the corresponding `file` + val testClassUpdated = (file as PsiClassOwner).classes.first() // only one class in the file + + // reformatting before creating reports due to + // SarifReport requires the final version of the generated tests code + runWriteCommandAction(testClassUpdated.project, "UtBot tests reformatting", null, { + reformat(model, file, testClassUpdated) + }) + unblockDocument(testClassUpdated.project, editor.document) + + // uploading formatted code + val testsCodeWithTestReportFormatted = + testsCodeWithTestReport.copy(generatedCode = file.text) + + // creating and saving reports + saveSarifAndTestReports( + testClassUpdated, + testCases, + model, + testsCodeWithTestReportFormatted, + reportsCountDown + ) - unblockDocument(testClass.project, editor.document) - // TODO: JIRA:1246 - display warnings if we rewrite the file - executeCommand(testClass.project, "Insert Generated Tests") { - editor.document.setText(generatedTestsCode) + unblockDocument(testClassUpdated.project, editor.document) + } + } } - unblockDocument(testClass.project, editor.document) - - // after committing the document the `testClass` is invalid in PsiTree, - // so we have to reload it from the corresponding `file` - val testClassUpdated = (file as PsiClassOwner).classes.first() // only one class in the file - - // reformatting before creating reports due to - // SarifReport requires the final version of the generated tests code - reformat(model, file, testClassUpdated) - unblockDocument(testClassUpdated.project, editor.document) - - // uploading formatted code - val testsCodeWithTestReportFormatted = testsCodeWithTestReport.copy(generatedCode = file.text) - - // creating and saving reports - saveSarifAndTestReports(testClassUpdated, testCases, model, testsCodeWithTestReportFormatted) - - unblockDocument(testClassUpdated.project, editor.document) } + //Note that reportsCountDown.countDown() has to be called in every generator implementation to complete whole process else -> TODO("Only model based code generator supported, but got: ${generator::class}") } } @@ -257,7 +323,8 @@ object TestGenerator { testClass: PsiClass, testCases: List, model: GenerateTestsModel, - testsCodeWithTestReport: TestsCodeWithTestReport + testsCodeWithTestReport: TestsCodeWithTestReport, + reportsCountDown: CountDownLatch ) { val project = model.project val generatedTestsCode = testsCodeWithTestReport.generatedCode @@ -274,6 +341,8 @@ object TestGenerator { message = "Cannot save Sarif report via generated tests: error occurred '${e.message}'", title = "Failed to save Sarif report" ) + } finally { + reportsCountDown.countDown() } try { @@ -294,7 +363,7 @@ object TestGenerator { } private fun saveTestsReport(testsCodeWithTestReport: TestsCodeWithTestReport, model: GenerateTestsModel) { - val testResourcesDirPath = model.testModule.getOrCreateTestResourcesPath() + val testResourcesDirPath = model.testModule.getOrCreateTestResourcesPath(model.testSourceRoot) require(testResourcesDirPath.exists()) { "Test resources directory $testResourcesDirPath does not exist" @@ -317,6 +386,8 @@ object TestGenerator { VfsUtil.createDirectories(parent.toString()) resultedReportedPath.toFile().writeText(testsCodeWithTestReport.testsGenerationReport.getFileContent()) + processInitialWarnings(testsCodeWithTestReport, model) + val notifyMessage = buildString { appendHtmlLine(testsCodeWithTestReport.testsGenerationReport.toString()) appendHtmlLine() @@ -334,7 +405,38 @@ object TestGenerator { """.trimIndent() appendHtmlLine(savedFileMessage) } - TestsReportNotifier.notify(notifyMessage) + TestsReportNotifier.notify(notifyMessage, model.project, model.testModule) + } + + private fun processInitialWarnings(testsCodeWithTestReport: TestsCodeWithTestReport, model: GenerateTestsModel) { + val hasInitialWarnings = model.forceMockHappened || model.hasTestFrameworkConflict + if (!hasInitialWarnings) { + return + } + + testsCodeWithTestReport.testsGenerationReport.apply { + summaryMessage = { "Unit tests for $classUnderTest were generated with warnings.
    " } + + if (model.forceMockHappened) { + initialWarnings.add { + """ + Warning: Some test cases were ignored, because no mocking framework is installed in the project.
    + Better results could be achieved by installing mocking framework. + """.trimIndent() + } + } + if (model.hasTestFrameworkConflict) { + initialWarnings.add { + """ + Warning: There are several test frameworks in the project. + To select run configuration, please refer to the documentation depending on the project build system: + Gradle, + Maven + or Idea. + """.trimIndent() + } + } + } } @Suppress("unused") diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SarifReportIdea.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SarifReportIdea.kt index d18326a6a8..19e757a190 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SarifReportIdea.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/sarif/SarifReportIdea.kt @@ -21,8 +21,8 @@ object SarifReportIdea { ) { // building the path to the report file val classFqn = testCases.firstOrNull()?.method?.clazz?.qualifiedName ?: return - val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath() - val reportFilePath = sarifReportsPath.resolve("${classFqnToPath(classFqn)}-utbot.sarif") + val sarifReportsPath = model.testModule.getOrCreateSarifReportsPath(model.testSourceRoot) + val reportFilePath = sarifReportsPath.resolve("${classFqnToPath(classFqn)}Report.sarif") // creating report related directory VfsUtil.createDirectoryIfMissing(reportFilePath.parent.toString()) diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Settings.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Settings.kt index 2e74dcf0e5..b5c200161b 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Settings.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/settings/Settings.kt @@ -32,6 +32,8 @@ import com.intellij.openapi.components.Storage import com.intellij.openapi.project.Project import com.intellij.util.xmlb.Converter import com.intellij.util.xmlb.annotations.OptionTag +import org.utbot.common.FileUtil +import java.util.concurrent.CompletableFuture import kotlin.reflect.KClass @State( @@ -153,6 +155,11 @@ class Settings(val project: Project) : PersistentStateComponent override fun getState(): State = state + override fun initializeComponent() { + super.initializeComponent() + CompletableFuture.runAsync { FileUtil.clearTempDirectory(UtSettings.daysLimitForTempFiles) } + } + override fun loadState(state: State) { this.state = state } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/ConfigureWindowCommon.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/ConfigureWindowCommon.kt new file mode 100644 index 0000000000..d682574bb0 --- /dev/null +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/ConfigureWindowCommon.kt @@ -0,0 +1,47 @@ +package org.utbot.intellij.plugin.ui + +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.DependencyScope +import com.intellij.openapi.roots.ExternalLibraryDescriptor +import com.intellij.openapi.roots.JavaProjectModelModificationService +import com.intellij.openapi.ui.Messages +import org.jetbrains.concurrency.Promise +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.intellij.plugin.ui.utils.LibrarySearchScope +import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary +import org.utbot.intellij.plugin.ui.utils.parseVersion + +fun createMockFrameworkNotificationDialog(title: String) = Messages.showYesNoDialog( + """Mock framework ${MockFramework.MOCKITO.displayName} is not installed into current module. + |Would you like to install it now?""".trimMargin(), + title, + "Yes", + "No", + Messages.getQuestionIcon(), +) + +fun configureMockFramework(project: Project, module: Module) { + val selectedMockFramework = MockFramework.MOCKITO + + val libraryInProject = + findFrameworkLibrary(project, module, selectedMockFramework, LibrarySearchScope.Project) + val versionInProject = libraryInProject?.libraryName?.parseVersion() + + selectedMockFramework.isInstalled = true + addDependency(project, module, mockitoCoreLibraryDescriptor(versionInProject)) + .onError { selectedMockFramework.isInstalled = false } +} + +/** + * Adds the dependency for selected framework via [JavaProjectModelModificationService]. + * + * Note that version restrictions will be applied only if they are present on target machine + * Otherwise latest release version will be installed. + */ +fun addDependency(project: Project, module: Module, libraryDescriptor: ExternalLibraryDescriptor): Promise { + return JavaProjectModelModificationService + .getInstance(project) + //this method returns JetBrains internal Promise that is difficult to deal with, but it is our way + .addDependency(module, libraryDescriptor, DependencyScope.TEST) +} 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 72ec516f20..6177131a0d 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 @@ -2,36 +2,6 @@ package org.utbot.intellij.plugin.ui -import org.utbot.common.PathUtil.toPath -import org.utbot.framework.UtSettings -import org.utbot.framework.codegen.ForceStaticMocking -import org.utbot.framework.codegen.Junit4 -import org.utbot.framework.codegen.Junit5 -import org.utbot.framework.codegen.NoStaticMocking -import org.utbot.framework.codegen.ParametrizedTestSource -import org.utbot.framework.codegen.StaticsMocking -import org.utbot.framework.codegen.TestFramework -import org.utbot.framework.codegen.TestNg -import org.utbot.framework.codegen.model.util.MOCKITO_EXTENSIONS_FILE_CONTENT -import org.utbot.framework.codegen.model.util.MOCKITO_EXTENSIONS_STORAGE -import org.utbot.framework.codegen.model.util.MOCKITO_MOCKMAKER_FILE_NAME -import org.utbot.framework.plugin.api.CodeGenerationSettingItem -import org.utbot.framework.plugin.api.CodegenLanguage -import org.utbot.framework.plugin.api.MockFramework -import org.utbot.framework.plugin.api.MockFramework.MOCKITO -import org.utbot.framework.plugin.api.MockStrategyApi -import org.utbot.framework.plugin.api.TreatOverflowAsError -import org.utbot.intellij.plugin.settings.Settings -import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton -import org.utbot.intellij.plugin.ui.utils.LibrarySearchScope -import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary -import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath -import org.utbot.intellij.plugin.ui.utils.kotlinTargetPlatform -import org.utbot.intellij.plugin.ui.utils.parseVersion -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots -import org.utbot.intellij.plugin.ui.utils.testResourceRootTypes -import org.utbot.intellij.plugin.ui.utils.testRootType -import org.utbot.intellij.plugin.util.AndroidApiHelper import com.intellij.codeInsight.hint.HintUtil import com.intellij.icons.AllIcons import com.intellij.ide.impl.ProjectNewWindowDoNotAskOption @@ -41,9 +11,7 @@ import com.intellij.openapi.components.service import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.projectRoots.JavaSdkVersion -import com.intellij.openapi.roots.DependencyScope -import com.intellij.openapi.roots.ExternalLibraryDescriptor -import com.intellij.openapi.roots.JavaProjectModelModificationService +import com.intellij.openapi.roots.ContentEntry import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.ui.configuration.ClasspathEditor import com.intellij.openapi.roots.ui.configuration.ProjectStructureConfigurable @@ -51,10 +19,14 @@ 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.ui.ValidationInfo import com.intellij.openapi.ui.popup.IconButton +import com.intellij.openapi.util.Computable +import com.intellij.openapi.vfs.StandardFileSystems import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore.urlToPath import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile import com.intellij.psi.PsiClass import com.intellij.psi.PsiManager import com.intellij.psi.PsiMethod @@ -85,11 +57,43 @@ import com.intellij.ui.layout.panel import com.intellij.util.IncorrectOperationException import com.intellij.util.io.exists import com.intellij.util.lang.JavaVersion +import com.intellij.util.ui.JBUI import com.intellij.util.ui.JBUI.Borders.empty import com.intellij.util.ui.JBUI.Borders.merge import com.intellij.util.ui.JBUI.scale import com.intellij.util.ui.JBUI.size +import com.intellij.util.ui.UIUtil import com.intellij.util.ui.components.BorderLayoutPanel +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.UtSettings +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.Junit4 +import org.utbot.framework.codegen.Junit5 +import org.utbot.framework.codegen.NoStaticMocking +import org.utbot.framework.codegen.ParametrizedTestSource +import org.utbot.framework.codegen.StaticsMocking +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.codegen.TestNg +import org.utbot.framework.codegen.model.util.MOCKITO_EXTENSIONS_FILE_CONTENT +import org.utbot.framework.codegen.model.util.MOCKITO_EXTENSIONS_STORAGE +import org.utbot.framework.codegen.model.util.MOCKITO_MOCKMAKER_FILE_NAME +import org.utbot.framework.plugin.api.CodeGenerationSettingItem +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockFramework.MOCKITO +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.api.TreatOverflowAsError +import org.utbot.intellij.plugin.settings.Settings +import org.utbot.intellij.plugin.ui.components.TestFolderComboWithBrowseButton +import org.utbot.intellij.plugin.ui.utils.LibrarySearchScope +import org.utbot.intellij.plugin.ui.utils.addSourceRootIfAbsent +import org.utbot.intellij.plugin.ui.utils.findFrameworkLibrary +import org.utbot.intellij.plugin.ui.utils.getOrCreateTestResourcesPath +import org.utbot.intellij.plugin.ui.utils.kotlinTargetPlatform +import org.utbot.intellij.plugin.ui.utils.parseVersion +import org.utbot.intellij.plugin.ui.utils.testResourceRootTypes +import org.utbot.intellij.plugin.ui.utils.testRootType +import org.utbot.intellij.plugin.util.AndroidApiHelper import java.awt.BorderLayout import java.awt.Color import java.nio.file.Files @@ -97,9 +101,12 @@ import java.nio.file.Path import java.nio.file.Paths import java.util.Objects import java.util.concurrent.TimeUnit +import javax.swing.DefaultComboBoxModel +import javax.swing.JComboBox +import javax.swing.JComponent +import javax.swing.JList +import javax.swing.JPanel import kotlin.streams.toList -import org.jetbrains.concurrency.Promise -import javax.swing.* private const val RECENTS_KEY = "org.utbot.recents" @@ -111,7 +118,8 @@ private const val MINIMUM_TIMEOUT_VALUE_IN_SECONDS = 1 class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(model.project) { companion object { - const val supportedSdkVersion = 8 + const val minSupportedSdkVersion = 8 + const val maxSupportedSdkVersion = 11 } private val membersTable = MemberSelectionTable(emptyList(), null) @@ -153,6 +161,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m init { title = "Generate tests with UtBot" + setResizable(false) init() } @@ -233,10 +242,15 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m contextHelpLabel?.let { add(it, BorderLayout.LINE_END) } }) + private fun findSdkVersion(): JavaVersion? { + val projectSdk = ModuleRootManager.getInstance(model.srcModule).sdk + return JavaVersion.tryParse(projectSdk?.versionString) + } + override fun createTitlePane(): JComponent? { val sdkVersion = findSdkVersion() //TODO:SAT-1571 investigate Android Studio specific sdk issues - if (sdkVersion?.feature == supportedSdkVersion || AndroidApiHelper.isAndroidStudio()) return null + if (sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion || AndroidApiHelper.isAndroidStudio()) return null isOKActionEnabled = false return SdkNotificationPanel(model, sdkVersion) } @@ -246,11 +260,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m return if (packageNames.size == 1) packageNames.first() else SAME_PACKAGE_LABEL } - private fun findSdkVersion(): JavaVersion? { - val projectSdk = ModuleRootManager.getInstance(model.testModule).sdk - return JavaVersion.tryParse(projectSdk?.versionString) - } - /** * A panel to inform user about incorrect jdk in project. * @@ -266,7 +275,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m addToLeft(JBLabel().apply { icon = AllIcons.Ide.FatalError text = if (sdkVersion != null) { - "SDK version $sdkVersion is not supported, use ${JavaSdkVersion.JDK_1_8}" + "SDK version $sdkVersion is not supported, use ${JavaSdkVersion.JDK_1_8} or ${JavaSdkVersion.JDK_11}." } else { "SDK is not defined" } @@ -286,10 +295,10 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m addHyperlinkListener { val projectStructure = ProjectStructureConfigurable.getInstance(model.project) val isEdited = ShowSettingsUtil.getInstance().editConfigurable(model.project, projectStructure) - { projectStructure.select(model.testModule.name, ClasspathEditor.getName(), true) } + { projectStructure.select(model.srcModule.name, ClasspathEditor.getName(), true) } val sdkVersion = findSdkVersion() - val sdkFixed = isEdited && sdkVersion?.feature == supportedSdkVersion + val sdkFixed = isEdited && sdkVersion?.feature in minSupportedSdkVersion..maxSupportedSdkVersion if (sdkFixed) { this@SdkNotificationPanel.isVisible = false isOKActionEnabled = true @@ -345,6 +354,34 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m private fun checkMembers(members: List) = members.forEach { it.isChecked = true } + private fun getTestRoot() : VirtualFile? { + model.testSourceRoot?.let { + if (it.isDirectory || it is FakeVirtualFile) return it + } + return null + } + + override fun doValidate(): ValidationInfo? { + val testRoot = getTestRoot() + ?: return ValidationInfo("Test source root is not configured", testSourceFolderField.childComponent) + + if (findReadOnlyContentEntry(testRoot) == null) { + return ValidationInfo("Test source root is located out of content entry", testSourceFolderField.childComponent) + } + + membersTable.tableHeader?.background = UIUtil.getTableBackground() + membersTable.background = UIUtil.getTableBackground() + if (membersTable.selectedMemberInfos.isEmpty()) { + membersTable.tableHeader?.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + membersTable.background = JBUI.CurrentTheme.Validator.errorBackgroundColor() + return ValidationInfo( + "Tick any methods to generate tests for", membersTable + ) + } + return null + } + + override fun doOKAction() { model.testPackageName = if (testPackageField.text != SAME_PACKAGE_LABEL) testPackageField.text else "" @@ -418,11 +455,10 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m * Creates test source root if absent and target packages for tests. */ private fun createTestRootAndPackages(): Boolean { + model.testSourceRoot = createDirectoryIfMissing(model.testSourceRoot) val testSourceRoot = model.testSourceRoot ?: return false if (model.testSourceRoot?.isDirectory != true) return false - - val rootExists = getOrCreateTestRoot(testSourceRoot) - if (rootExists) { + if (getOrCreateTestRoot(testSourceRoot)) { if (cbSpecifyTestPackage.isSelected) { createSelectedPackage(testSourceRoot) } else { @@ -430,10 +466,24 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } return true } - return false } + 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 createPackagesByClasses(testSourceRoot: VirtualFile) { val packageNames = model.srcClasses.map { it.packageName }.sortedBy { it.length } for (packageName in packageNames) { @@ -460,19 +510,33 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m "Generation error" ) - private fun getOrCreateTestRoot(testSourceRoot: VirtualFile): Boolean { - val modifiableModel = ModuleRootManager.getInstance(model.testModule).modifiableModel - val contentEntry = modifiableModel.contentEntries + private fun findReadOnlyContentEntry(testSourceRoot: VirtualFile?): ContentEntry? { + if (testSourceRoot == null) return null + if (testSourceRoot is FakeVirtualFile) { + return findReadOnlyContentEntry(testSourceRoot.parent) + } + return ModuleRootManager.getInstance(model.testModule).contentEntries .filterNot { it.file == null } - .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, true) } - ?: return false - - VfsUtil.createDirectoryIfMissing(urlToPath(testSourceRoot.url)) - - contentEntry.addSourceFolder(testSourceRoot.url, codegenLanguages.item.testRootType()) - WriteCommandAction.runWriteCommandAction(model.project) { modifiableModel.commit() } + .firstOrNull { VfsUtil.isAncestor(it.file!!, testSourceRoot, false) } + } - return true + 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, true) } + ?: return false + + contentEntry.addSourceRootIfAbsent( + modifiableModel, + testSourceRoot.url, + codegenLanguages.item.testRootType() + ) + return true + } finally { + if (modifiableModel.isWritable && !modifiableModel.isDisposed) modifiableModel.dispose() + } } private fun createPackageWrapper(packageName: String?): PackageWrapper = @@ -524,14 +588,16 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m if (frameworkNotInstalled && createTestFrameworkNotificationDialog() == Messages.YES) { configureTestFramework() } + + model.hasTestFrameworkConflict = TestFramework.allItems.count { it.isInstalled } > 1 } private fun configureMockFrameworkIfRequired() { val frameworkNotInstalled = mockStrategies.item != MockStrategyApi.NO_MOCKS && !MOCKITO.isInstalled - if (frameworkNotInstalled && createMockFrameworkNotificationDialog() == Messages.YES) { - configureMockFramework() + if (frameworkNotInstalled && createMockFrameworkNotificationDialog(title) == Messages.YES) { + configureMockFramework(model.project, model.testModule) } } @@ -558,24 +624,12 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } selectedTestFramework.isInstalled = true - addDependency(libraryDescriptor) + addDependency(model.project, model.testModule, libraryDescriptor) .onError { selectedTestFramework.isInstalled = false } } - private fun configureMockFramework() { - val selectedMockFramework = MOCKITO - - val libraryInProject = - findFrameworkLibrary(model.project, model.testModule, selectedMockFramework, LibrarySearchScope.Project) - val versionInProject = libraryInProject?.libraryName?.parseVersion() - - selectedMockFramework.isInstalled = true - addDependency(mockitoCoreLibraryDescriptor(versionInProject)) - .onError { selectedMockFramework.isInstalled = false } - } - private fun configureStaticMocking() { - val testResourcesUrl = model.testModule.getOrCreateTestResourcesPath() + val testResourcesUrl = model.testModule.getOrCreateTestResourcesPath(model.testSourceRoot) configureMockitoResources(testResourcesUrl) val staticsMockingValue = staticsMocking.item @@ -602,19 +656,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } - /** - * Adds the dependency for selected framework via [JavaProjectModelModificationService]. - * - * Note that version restrictions will be applied only if they are present on target machine - * Otherwise latest release version will be installed. - */ - private fun addDependency(libraryDescriptor: ExternalLibraryDescriptor): Promise { - return JavaProjectModelModificationService - .getInstance(model.project) - //this method returns JetBrains internal Promise that is difficult to deal with, but it is our way - .addDependency(model.testModule, libraryDescriptor, DependencyScope.TEST) - } - private fun createTestFrameworkNotificationDialog() = Messages.showYesNoDialog( """Selected test framework ${testFrameworks.item.displayName} is not installed into current module. |Would you like to install it now?""".trimMargin(), @@ -624,15 +665,6 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m Messages.getQuestionIcon(), ) - private fun createMockFrameworkNotificationDialog() = Messages.showYesNoDialog( - """Mock framework ${MOCKITO.displayName} is not installed into current module. - |Would you like to install it now?""".trimMargin(), - title, - "Yes", - "No", - Messages.getQuestionIcon(), - ) - private fun createStaticsMockingNotificationDialog() = Messages.showYesNoDialog( """A framework ${MOCKITO.displayName} is not configured to mock static methods. |Would you like to configure it now?""".trimMargin(), @@ -702,9 +734,14 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m itemsToHelpTooltip.forEach { (box, tooltip) -> box.setHelpTooltipTextChanger(tooltip) } testSourceFolderField.childComponent.addActionListener { event -> - val item = (event.source as JComboBox<*>).selectedItem as String - - pathToFile(item)?.let { model.testSourceRoot = it } + with((event.source as JComboBox<*>).selectedItem) { + if (this is VirtualFile) { + model.testSourceRoot = this@with + } + else { + model.testSourceRoot = null + } + } } mockStrategies.addActionListener { event -> @@ -727,9 +764,9 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m parametrizedTestSources.addActionListener { event -> val comboBox = event.source as ComboBox<*> - val item = comboBox.item as ParametrizedTestSource + val parametrizedTestSource = comboBox.item as ParametrizedTestSource - val areMocksSupported = item == ParametrizedTestSource.DO_NOT_PARAMETRIZE + val areMocksSupported = parametrizedTestSource == ParametrizedTestSource.DO_NOT_PARAMETRIZE mockStrategies.isEnabled = areMocksSupported staticsMocking.isEnabled = areMocksSupported && mockStrategies.item != MockStrategyApi.NO_MOCKS @@ -740,7 +777,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m staticsMocking.item = NoStaticMocking } - updateTestFrameworksList(item) + updateTestFrameworksList(parametrizedTestSource) } cbSpecifyTestPackage.addActionListener { @@ -756,35 +793,33 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } } - private fun pathToFile(path: String): VirtualFile? { - val relativePath = path.substring(".../".length).replace('\\', '/') - return model.testModule - .suitableTestSourceRoots() - .firstOrNull { it.path.contains(relativePath) } - } - - private lateinit var currentFrameworkItem: TestFramework //We would like to remove JUnit4 from framework list in parametrized mode private fun updateTestFrameworksList(parametrizedTestSource: ParametrizedTestSource) { //We do not support parameterized tests for JUnit4 - val enabledTestFrameworks = when (parametrizedTestSource) { + var enabledTestFrameworks = when (parametrizedTestSource) { ParametrizedTestSource.DO_NOT_PARAMETRIZE -> TestFramework.allItems ParametrizedTestSource.PARAMETRIZE -> TestFramework.allItems.filterNot { it == Junit4 } } - enabledTestFrameworks.forEach { - it.isInstalled = findFrameworkLibrary(model.project, model.testModule, it) != null + //Will be removed after gradle-intelij-plugin version update upper than 2020.2 + //TestNg will be reverted after https://github.com/UnitTestBot/UTBotJava/issues/309 + if (findSdkVersion()?.let { it.feature < 11 } == true) { + enabledTestFrameworks = enabledTestFrameworks.filterNot { it == TestNg } } - val defaultItem = when (parametrizedTestSource) { + var defaultItem = when (parametrizedTestSource) { ParametrizedTestSource.DO_NOT_PARAMETRIZE -> TestFramework.defaultItem ParametrizedTestSource.PARAMETRIZE -> TestFramework.parametrizedDefaultItem } + enabledTestFrameworks.forEach { + it.isInstalled = findFrameworkLibrary(model.project, model.testModule, it) != null + if (it.isInstalled && !defaultItem.isInstalled) defaultItem = it + } testFrameworks.model = DefaultComboBoxModel(enabledTestFrameworks.toTypedArray()) - testFrameworks.item = if (currentFrameworkItem in enabledTestFrameworks) currentFrameworkItem else defaultItem + testFrameworks.item = if (currentFrameworkItem in enabledTestFrameworks && currentFrameworkItem.isInstalled) currentFrameworkItem else defaultItem testFrameworks.renderer = object : ColoredListCellRenderer() { override fun customizeCellRenderer( list: JList, value: TestFramework?, @@ -843,7 +878,7 @@ class GenerateTestsDialogWindow(val model: GenerateTestsModel) : DialogWrapper(m } private fun staticsMockingConfigured(): Boolean { - val entries = ModuleRootManager.getInstance(model.testModule).modifiableModel.contentEntries + val entries = ModuleRootManager.getInstance(model.testModule).contentEntries val hasEntriesWithoutResources = entries .filterNot { it.sourceFolders.any { f -> f.rootType in testResourceRootTypes } } .isNotEmpty() diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsModel.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsModel.kt index 190c79ea65..d2fcf6436c 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsModel.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/GenerateTestsModel.kt @@ -26,7 +26,9 @@ data class GenerateTestsModel( var srcClasses: Set, var selectedMethods: Set?, var timeout:Long, - var generateWarningsForStaticMocking: Boolean = false + var generateWarningsForStaticMocking: Boolean = false, + var forceMockHappened: Boolean = false, + var hasTestFrameworkConflict: Boolean = false, ) { var testSourceRoot: VirtualFile? = null var testPackageName: String? = null diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt index ce93076abb..ffd0af96d9 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/Notifications.kt @@ -1,11 +1,25 @@ package org.utbot.intellij.plugin.ui +import com.intellij.ide.util.PropertiesComponent +import com.intellij.notification.Notification import com.intellij.notification.NotificationDisplayType import com.intellij.notification.NotificationGroup import com.intellij.notification.NotificationListener import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.StartupActivity +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.ui.popup.Balloon +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.GotItMessage +import com.intellij.ui.awt.RelativePoint +import com.intellij.util.ui.JBFont +import java.awt.Point +import javax.swing.event.HyperlinkEvent abstract class Notifier { protected abstract val notificationType: NotificationType @@ -65,6 +79,7 @@ object UnsupportedTestFrameworkNotifier : ErrorNotifier() { abstract class UrlNotifier : Notifier() { protected abstract val titleText: String + protected abstract val urlOpeningListener: NotificationListener override fun notify(info: String, project: Project?, module: Module?) { notificationGroup @@ -72,7 +87,7 @@ abstract class UrlNotifier : Notifier() { titleText, content(project, module, info), notificationType, - NotificationListener.UrlOpeningListener(false) + urlOpeningListener, ).notify(project) } } @@ -87,6 +102,8 @@ object SarifReportNotifier : InformationUrlNotifier() { override val titleText: String = "" // no title + override val urlOpeningListener: NotificationListener = NotificationListener.UrlOpeningListener(false) + override fun content(project: Project?, module: Module?, info: String): String = info } @@ -95,5 +112,71 @@ object TestsReportNotifier : InformationUrlNotifier() { override val titleText: String = "Report of the unit tests generation via UtBot" - override fun content(project: Project?, module: Module?, info: String): String = info + override val urlOpeningListener: TestReportUrlOpeningListener = TestReportUrlOpeningListener() + + override fun content(project: Project?, module: Module?, info: String): String { + // Remember last project and module to use them for configurations. + urlOpeningListener.project = project + urlOpeningListener.module = module + return info + } +} + +/** + * Listener that handles URLs starting with [prefix], like "#utbot/configure-mockito". + * + * Current implementation + */ +class TestReportUrlOpeningListener: NotificationListener.Adapter() { + companion object { + const val prefix = "#utbot/" + const val mockitoSuffix = "configure-mockito" + } + private val defaultListener = NotificationListener.UrlOpeningListener(false) + + // Last project and module to be able to use them when activated for configuration tasks. + var project: Project? = null + var module: Module? = null + + override fun hyperlinkActivated(notification: Notification, e: HyperlinkEvent) { + val description = e.description + if (description.startsWith(prefix)) { + handleDescription(description.removePrefix(prefix)) + } else { + return defaultListener.hyperlinkUpdate(notification, e) + } + } + + private fun handleDescription(descriptionSuffix: String) { + when { + descriptionSuffix.startsWith(mockitoSuffix) -> { + project?.let { project -> module?.let { module -> + if (createMockFrameworkNotificationDialog("Configure mock framework") == Messages.YES) { + configureMockFramework(project, module) + } + } ?: error("Could not configure mock framework: module is null for project $project") + } ?: error("Could not configure mock framework: project is null") + } + else -> error("No such command with #utbot prefix: $descriptionSuffix") + } + } +} + +object GotItTooltipActivity : StartupActivity { + private const val KEY = "UTBot.GotItMessageWasShown" + override fun runActivity(project: Project) { + if (PropertiesComponent.getInstance().isTrueValue(KEY)) return + ApplicationManager.getApplication().invokeLater { + val shortcut = ActionManager.getInstance() + .getKeyboardShortcut("org.utbot.intellij.plugin.ui.actions.GenerateTestsAction")?:return@invokeLater + val shortcutText = KeymapUtil.getShortcutText(shortcut) + val message = GotItMessage.createMessage("UTBot is ready!", + "
    " + + "You can get test coverage for methods, Java classes,
    and even for whole source roots
    with $shortcutText
    ") + message.setCallback { PropertiesComponent.getInstance().setValue(KEY, true) } + WindowManager.getInstance().getFrame(project)?.rootPane?.let { + message.show(RelativePoint(it, Point(it.width, it.height)), Balloon.Position.above) + } + } + } } diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/UtTestsDialogProcessor.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/UtTestsDialogProcessor.kt index 6e23545a9c..02137c6824 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/UtTestsDialogProcessor.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/UtTestsDialogProcessor.kt @@ -40,6 +40,7 @@ import java.nio.file.Paths import java.util.concurrent.TimeUnit import mu.KotlinLogging import org.jetbrains.kotlin.idea.util.module +import org.utbot.engine.util.mockListeners.ForceMockListener import org.utbot.intellij.plugin.error.showErrorDialogLater object UtTestsDialogProcessor { @@ -51,24 +52,27 @@ object UtTestsDialogProcessor { srcClasses: Set, focusedMethod: MemberInfo?, ) { - val dialog = createDialog(project, srcClasses, focusedMethod) - if (!dialog.showAndGet()) { - return + createDialog(project, srcClasses, focusedMethod)?.let { + if (it.showAndGet()) createTests(project, it.model) } - - createTests(project, dialog.model) } private fun createDialog( project: Project, srcClasses: Set, focusedMethod: MemberInfo?, - ): GenerateTestsDialogWindow { + ): GenerateTestsDialogWindow? { val srcModule = findSrcModule(srcClasses) val testModule = srcModule.testModule(project) JdkPathService.jdkPathProvider = PluginJdkPathProvider(project, testModule) - val jdkVersion = testModule.jdkVersion() + val jdkVersion = try { + testModule.jdkVersion() + } catch (e: IllegalStateException) { + // Just ignore it here, notification will be shown in + // org.utbot.intellij.plugin.ui.utils.ModuleUtilsKt.jdkVersionBy + return null + } return GenerateTestsDialogWindow( GenerateTestsModel( @@ -149,6 +153,7 @@ object UtTestsDialogProcessor { .executeSynchronously() withSubstitutionCondition(shouldSubstituteStatics) { + val mockFrameworkInstalled = model.mockFramework?.isInstalled ?: true val codeGenerator = CodeGenerator( searchDirectory = searchDirectory, mockStrategy = model.mockStrategy, @@ -159,6 +164,14 @@ object UtTestsDialogProcessor { chosenClassesToMockAlways = model.chosenClassesToMockAlways ) { indicator.isCanceled } + val forceMockListener = if (!mockFrameworkInstalled) { + ForceMockListener().apply { + codeGenerator.generator.configureEngine = { engine -> engine.attachMockListener(this) } + } + } else { + null + } + val notEmptyCases = withUtContext(context) { codeGenerator .generateForSeveralMethods(methods, model.timeout) @@ -175,6 +188,10 @@ object UtTestsDialogProcessor { testCasesByClass[srcClass] = notEmptyCases } + forceMockListener?.run { + model.forceMockHappened = forceMockHappened + } + timerHandler.cancel(true) } processedClasses++ 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 7df71aa2f5..6b079e094f 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 @@ -58,6 +58,7 @@ class GenerateTestsAction : AnAction() { e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY)?.let { srcClasses += getAllClasses(project, it) } + srcClasses.removeIf { it.isInterface } var commonSourceRoot = null as VirtualFile? for (srcClass in srcClasses) { if (commonSourceRoot == null) { diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt index b03960bd0c..c473054696 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/components/TestFolderComboWithBrowseButton.kt @@ -1,17 +1,23 @@ package org.utbot.intellij.plugin.ui.components -import org.utbot.common.PathUtil -import org.utbot.intellij.plugin.ui.GenerateTestsModel -import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots -import com.intellij.ide.ui.laf.darcula.DarculaUIUtil import com.intellij.openapi.application.ReadAction import com.intellij.openapi.fileChooser.FileChooser import com.intellij.openapi.fileChooser.FileChooserDescriptor import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.newvfs.impl.FakeVirtualFile +import com.intellij.ui.ColoredListCellRenderer import com.intellij.ui.ComboboxWithBrowseButton +import com.intellij.ui.SimpleTextAttributes import com.intellij.util.ArrayUtil +import java.io.File import javax.swing.DefaultComboBoxModel +import javax.swing.JList +import org.utbot.common.PathUtil +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.intellij.plugin.ui.GenerateTestsModel +import org.utbot.intellij.plugin.ui.utils.addDedicatedTestRoot +import org.utbot.intellij.plugin.ui.utils.suitableTestSourceRoots class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : ComboboxWithBrowseButton() { @@ -19,9 +25,29 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C init { childComponent.isEditable = false + childComponent.renderer = object : ColoredListCellRenderer() { + override fun customizeCellRenderer( + list: JList, + value: Any?, + index: Int, + selected: Boolean, + hasFocus: Boolean + ) { + if (value is String) { + append(value) + return + } + if (value is VirtualFile) { + append(formatUrl(value, model)) + } + if (value is FakeVirtualFile) { + append(" (will be created)", SimpleTextAttributes.ERROR_ATTRIBUTES) + } + } + } - val testRoots = model.testModule.suitableTestSourceRoots() - + val testRoots = model.testModule.suitableTestSourceRoots(CodegenLanguage.JAVA).toMutableList() + model.testModule.addDedicatedTestRoot(testRoots) if (testRoots.isNotEmpty()) { configureRootsCombo(testRoots) } else { @@ -34,11 +60,11 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C model.testSourceRoot = it if (childComponent.itemCount == 1 && childComponent.selectedItem == SET_TEST_FOLDER) { - newItemList(setOf(formatUrl(it, model))) + newItemList(setOf(it)) } else { //Prepend and select newly added test root - val testRootItems = linkedSetOf(formatUrl(it, model)) - testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as String} + val testRootItems = linkedSetOf(it) + testRootItems += (0 until childComponent.itemCount).map { i -> childComponent.getItemAt(i) as VirtualFile} newItemList(testRootItems) } } @@ -59,17 +85,19 @@ class TestFolderComboWithBrowseButton(private val model: GenerateTestsModel) : C val selectedRoot = testRoots.first() model.testSourceRoot = selectedRoot - newItemList(testRoots.map { root -> formatUrl(root, model) }.toSet()) + newItemList(testRoots.toSet()) } - private fun newItemList(comboItems: Set) { + private fun newItemList(comboItems: Set) { childComponent.model = DefaultComboBoxModel(ArrayUtil.toObjectArray(comboItems)) - childComponent.putClientProperty("JComponent.outline", - if (comboItems.isNotEmpty() && !comboItems.contains(SET_TEST_FOLDER)) null else DarculaUIUtil.Outline.error) } private fun formatUrl(virtualFile: VirtualFile, model: GenerateTestsModel): String { - var directoryUrl = virtualFile.presentableUrl + var directoryUrl = if (virtualFile is FakeVirtualFile) { + virtualFile.parent.presentableUrl + File.separatorChar + virtualFile.name + } else { + virtualFile.presentableUrl + } @Suppress("DEPRECATION") val projectHomeUrl = model.project.baseDir.presentableUrl diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt index 33bd3c1cdb..2d4fbbf79f 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/ModuleUtils.kt @@ -14,16 +14,20 @@ 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 com.intellij.openapi.roots.ContentEntry +import com.intellij.openapi.roots.ModifiableRootModel import com.intellij.openapi.roots.ModuleRootManager import com.intellij.openapi.roots.SourceFolder import com.intellij.openapi.roots.TestModuleProperties import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +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 @@ -35,7 +39,7 @@ private val logger = KotlinLogging.logger {} */ fun Module.jdkVersion(): JavaSdkVersion { val moduleRootManager = ModuleRootManager.getInstance(this) - val sdk = moduleRootManager.sdk ?: error("No sdk found for module $this") // TODO: get sdk from project? + val sdk = moduleRootManager.sdk return jdkVersionBy(sdk) } @@ -64,18 +68,17 @@ fun Module.suitableTestSourceFolders(): List = * * If no roots exist, our suggestion is a folder named "resources" in the entry root. */ -fun Module.getOrCreateTestResourcesPath(): Path { - val testResourcesUrl = getOrCreateTestResourcesUrl(this) +fun Module.getOrCreateTestResourcesPath(testSourceRoot: VirtualFile?): Path { + val testResourcesUrl = getOrCreateTestResourcesUrl(this, testSourceRoot) return VfsUtilCore.urlToPath(testResourcesUrl).toPath() } /** * Gets a path to Sarif reports directory or creates it. - * */ -fun Module.getOrCreateSarifReportsPath(): Path { - val testResourcesPath = this.getOrCreateTestResourcesPath() - return "$testResourcesPath/sarif/".toPath() +fun Module.getOrCreateSarifReportsPath(testSourceRoot: VirtualFile?): Path { + val testResourcesPath = this.getOrCreateTestResourcesPath(testSourceRoot) + return "$testResourcesPath/utbot-sarif-report/".toPath() } /** @@ -113,7 +116,7 @@ private fun findPotentialModuleForTests(project: Project, srcModule: Module): Mo /** * Finds all suitable test root virtual files. */ -private fun Module.suitableTestSourceRoots(codegenLanguage: CodegenLanguage): List { +fun Module.suitableTestSourceRoots(codegenLanguage: CodegenLanguage): List { val sourceRootsInModule = suitableTestSourceFolders(codegenLanguage).mapNotNull { it.file } if (sourceRootsInModule.isNotEmpty()) { @@ -137,52 +140,110 @@ private fun Module.suitableTestSourceFolders(codegenLanguage: CodegenLanguage): return sourceFolders .filterNot { it.isForGeneratedSources() } - .filter { folder -> folder.rootType == codegenLanguage.testRootType() } + .filter { it.rootType == codegenLanguage.testRootType() } + // Heuristics: User is more likely to choose the shorter path + .sortedBy { it.url.length } +} + +private const val dedicatedTestSourceRootName = "utbot_tests" +fun Module.addDedicatedTestRoot(testSourceRoots: MutableList): VirtualFile? { + // Dedicated test root already exists + // OR it looks like standard structure of Gradle project where 'unexpected' test roots won't work + if (testSourceRoots.any { file -> + file.name == dedicatedTestSourceRootName || file.path.endsWith("src/test/java") + }) return null + + val moduleInstance = ModuleRootManager.getInstance(this) + val testFolder = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } + .firstOrNull { it.rootType in testSourceRootTypes } + (testFolder?.let { testFolder.file?.parent } ?: (testFolder?.contentEntry + ?: moduleInstance.contentEntries.first()).file ?: moduleFile)?.let { + val file = FakeVirtualFile(it, dedicatedTestSourceRootName) + testSourceRoots.add(file) + // We return "true" IFF it's case of not yet created fake directory + return if (VfsUtil.findRelativeFile(it, dedicatedTestSourceRootName) == null) file else null + } + return null } private const val resourcesSuffix = "/resources" -private fun getOrCreateTestResourcesUrl(module: Module): String { - val moduleInstance = ModuleRootManager.getInstance(module) - val sourceFolders = moduleInstance.contentEntries.flatMap { it.sourceFolders.toList() } +private fun getOrCreateTestResourcesUrl(module: Module, testSourceRoot: VirtualFile?): String { + val rootModel = ModuleRootManager.getInstance(module).modifiableModel + try { + val sourceFolders = rootModel.contentEntries.flatMap { it.sourceFolders.toList() } - val testResourcesFolder = sourceFolders.firstOrNull { it.rootType in testResourceRootTypes } - if (testResourcesFolder != null) { - return testResourcesFolder.url - } + val testResourcesFolder = sourceFolders + .filter { sourceFolder -> + sourceFolder.rootType in testResourceRootTypes && !sourceFolder.isForGeneratedSources() + } + // taking the source folder that has the maximum common prefix + // with `testSourceRoot`, which was selected by the user + .maxBy { sourceFolder -> + val sourceFolderPath = sourceFolder.file?.path ?: "" + val testSourceRootPath = testSourceRoot?.path ?: "" + sourceFolderPath.commonPrefixWith(testSourceRootPath).length + } + if (testResourcesFolder != null) { + return testResourcesFolder.url + } - val testFolder = sourceFolders.firstOrNull { f -> f.rootType in testSourceRootTypes } - val contentEntry = testFolder?.contentEntry ?: moduleInstance.contentEntries.first() + val testFolder = sourceFolders.firstOrNull { it.rootType in testSourceRootTypes } + val contentEntry = testFolder?.contentEntry ?: rootModel.contentEntries.first() - val parentFolderUrl = testFolder?.let { getParentPath(testFolder.url) } - val testResourcesUrl = - if (parentFolderUrl != null) "${parentFolderUrl}$resourcesSuffix" else "${contentEntry.url}$resourcesSuffix" + val parentFolderUrl = testFolder?.let { getParentPath(testFolder.url) } + val testResourcesUrl = + if (parentFolderUrl != null) "${parentFolderUrl}$resourcesSuffix" else "${contentEntry.url}$resourcesSuffix" - val codegenLanguage = - if (testFolder?.rootType == TestResourceKotlinRootType) CodegenLanguage.KOTLIN else CodegenLanguage.JAVA + val codegenLanguage = + if (testFolder?.rootType == TestResourceKotlinRootType) CodegenLanguage.KOTLIN else CodegenLanguage.JAVA - try { - WriteCommandAction.runWriteCommandAction(module.project) { - contentEntry.addSourceFolder(testResourcesUrl, codegenLanguage.testResourcesRootType()) - moduleInstance.modifiableModel.commit() - VfsUtil.createDirectoryIfMissing(VfsUtilCore.urlToPath(testResourcesUrl)) + try { + contentEntry.addSourceRootIfAbsent(rootModel, testResourcesUrl, codegenLanguage.testResourcesRootType()) + } catch (e: java.lang.IllegalStateException) { + // Hack to avoid unmodifiable ModuleBridge testModule on Android SAT-1536. + workaround(WorkaroundReason.HACK) { + logger.info("Error during SARIF report generation: $e") + return testFolder!!.url + } } + + return testResourcesUrl + } finally { + if (!rootModel.isDisposed && rootModel.isWritable) rootModel.dispose() + } +} + +fun ContentEntry.addSourceRootIfAbsent( + model: ModifiableRootModel, + sourceRootUrl: String, + type: JpsModuleSourceRootType<*> +) { + getSourceFolders(type).find { it.url == sourceRootUrl }?.apply { + model.dispose() + return } - catch (e: java.lang.IllegalStateException) { - // Hack to avoid unmodifiable ModuleBridge testModule on Android SAT-1536. - workaround(WorkaroundReason.HACK) { - logger.info("Error during SARIF report generation: $e") - return testFolder!!.url + WriteCommandAction.runWriteCommandAction(rootModel.module.project) { + try { + VfsUtil.createDirectoryIfMissing(VfsUtilCore.urlToPath(sourceRootUrl)) + addSourceFolder(sourceRootUrl, type) + model.commit() + } catch (e: Exception) { + logger.error { e } + model.dispose() } } - - return testResourcesUrl } /** * Obtain JDK version and make sure that it is JDK8 or JDK11 */ -private fun jdkVersionBy(sdk: Sdk): JavaSdkVersion { +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) diff --git a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt index d14d770e4c..44d8b36876 100644 --- a/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt +++ b/utbot-intellij/src/main/kotlin/org/utbot/intellij/plugin/ui/utils/RootUtils.kt @@ -13,9 +13,9 @@ import org.jetbrains.kotlin.config.TestResourceKotlinRootType import org.jetbrains.kotlin.config.TestSourceKotlinRootType val sourceRootTypes: Set> = setOf(JavaSourceRootType.SOURCE, SourceKotlinRootType) -val testSourceRootTypes: Set> = setOf(JavaSourceRootType.TEST_SOURCE, TestSourceKotlinRootType) -val resourceRootTypes: Set> = setOf(JavaResourceRootType.RESOURCE, ResourceKotlinRootType) -val testResourceRootTypes: Set> = setOf(JavaResourceRootType.TEST_RESOURCE, TestResourceKotlinRootType) +val testSourceRootTypes: Set> = setOf(JavaSourceRootType.TEST_SOURCE, TestSourceKotlinRootType) +val resourceRootTypes: Set> = setOf(JavaResourceRootType.RESOURCE, ResourceKotlinRootType) +val testResourceRootTypes: Set> = setOf(JavaResourceRootType.TEST_RESOURCE, TestResourceKotlinRootType) /** * Defines test root type for selected codegen language. @@ -43,4 +43,4 @@ fun SourceFolder.isForGeneratedSources(): Boolean { val resourceProperties = jpsElement.getProperties(resourceRootTypes + testResourceRootTypes) return properties?.isForGeneratedSources == true && resourceProperties?.isForGeneratedSources == true -} \ 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 e91b7f849d..7d006496d0 100644 --- a/utbot-intellij/src/main/resources/META-INF/plugin.xml +++ b/utbot-intellij/src/main/resources/META-INF/plugin.xml @@ -6,7 +6,7 @@ utbot.org UnitTestBot plugin for IntelliJ IDEA for Java - 2022.5-alpha + 2022.7-beta com.intellij.modules.platform com.intellij.modules.java org.jetbrains.kotlin @@ -21,7 +21,7 @@ description="Cover code with auto-generated tests"> - + @@ -30,14 +30,44 @@ - + + + unit tests with a single action! +
    +
    + The UTBot engine goes through your code instructions and generates regression tests. +
    +
    + The engine finds potential problems in your code: +
    +
    +
      +
    • exceptions
    • +
    • hangs
    • +
    • overflows
    • +
    • and even native crashes
    • +
    +
    + They are not a surprise for you anymore. The engine will find the problems and generate tests for them. +
    +
    + The engine carefully selects tests to maximize statement and branch coverage. Our credo is to maximize test coverage and minimize tests number. +
    +
    + You can try the engine online without installation. +
    +
    + Got ideas? Let us know or become a contributor on our GitHub page +
    +
    + Found an issue? Please, submit it here. ]]>
    diff --git a/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg b/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg index 643edc6f85..08d64eb8d1 100644 --- a/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg +++ b/utbot-intellij/src/main/resources/META-INF/pluginIcon.svg @@ -76,7 +76,7 @@ - + + + + + + \ No newline at end of file diff --git a/utbot-junit-contest/build.gradle b/utbot-junit-contest/build.gradle index eb766fab19..82dd1e1cb9 100644 --- a/utbot-junit-contest/build.gradle +++ b/utbot-junit-contest/build.gradle @@ -2,6 +2,10 @@ apply from: "${rootProject.projectDir}/gradle/include/jvm-project.gradle" apply plugin: 'jacoco' +configurations { + fetchInstrumentationJar +} + compileJava { options.compilerArgs << '-XDignore.symbol.file' } @@ -50,8 +54,6 @@ dependencies { api project(":utbot-framework") api project(":utbot-analytics") - api project(":utbot-instrumentation") - implementation "com.github.UnitTestBot:soot:${soot_commit_hash}" implementation group: 'org.apache.commons', name: 'commons-exec', version: '1.2' implementation group: 'commons-io', name: 'commons-io', version: commons_io_version @@ -64,6 +66,13 @@ dependencies { testImplementation group: 'org.mockito', name: 'mockito-core', version: '4.2.0' testImplementation group: 'org.mockito', name: 'mockito-inline', version: '4.2.0' testImplementation 'junit:junit:4.13.2' + fetchInstrumentationJar project(path: ':utbot-instrumentation', configuration:'instrumentationArchive') +} + +processResources { + from(configurations.fetchInstrumentationJar) { + into "lib" + } } jar { dependsOn classes diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt index 06ba1e20eb..94e67f0dce 100644 --- a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimator.kt @@ -1,5 +1,8 @@ package org.utbot.contest +import mu.KotlinLogging +import org.utbot.analytics.EngineAnalyticsContext +import org.utbot.analytics.Predictors import org.utbot.common.FileUtil import org.utbot.common.bracket import org.utbot.common.info @@ -13,6 +16,8 @@ import org.utbot.contest.Paths.evosuiteReportFile import org.utbot.contest.Paths.jarsDir import org.utbot.contest.Paths.moduleTestDir import org.utbot.contest.Paths.outputDir +import org.utbot.features.FeatureExtractorFactoryImpl +import org.utbot.features.FeatureProcessorWithStatesRepetitionFactory import org.utbot.framework.plugin.api.util.UtContext import org.utbot.framework.plugin.api.util.id import org.utbot.framework.plugin.api.util.withUtContext @@ -29,16 +34,18 @@ import java.util.zip.ZipInputStream import kotlin.concurrent.thread import kotlin.math.min import kotlin.system.exitProcess -import mu.KotlinLogging +import org.utbot.framework.JdkPathService +import org.utbot.predictors.StateRewardPredictorFactoryImpl +import org.utbot.framework.PathSelectorType +import org.utbot.framework.UtSettings private val logger = KotlinLogging.logger {} private val classPathSeparator = System.getProperty("path.separator") -private val javaHome = if (System.getProperty("user.name") == "d00555580") { - "C:/Program Files/Java/jdk1.8.0_241" -} else { - System.getenv("JAVA_HOME") -} +//To hack it to debug something be like Duke +// if (System.getProperty("user.name") == "duke") my_path else "JAVA_HOME" +private val javaHome = System.getenv("JAVA_HOME") + private val javacCmd = "$javaHome/bin/javac" private val javaCmd = "$javaHome/bin/java" @@ -293,6 +300,7 @@ fun main(args: Array) { tools = listOf(Tool.UtBot) } + JdkPathService.jdkPathProvider = ContestEstimatorJdkPathProvider(javaHome) runEstimator(estimatorArgs, methodFilter, projectFilter, processedClassesThreshold, tools) } @@ -320,6 +328,17 @@ fun runEstimator( // Predictors.smtIncremental = UtBotTimePredictorIncremental() // Predictors.testName = StatementUniquenessPredictor() + EngineAnalyticsContext.featureProcessorFactory = FeatureProcessorWithStatesRepetitionFactory() + EngineAnalyticsContext.featureExtractorFactory = FeatureExtractorFactoryImpl() + EngineAnalyticsContext.stateRewardPredictorFactory = StateRewardPredictorFactoryImpl() + if (UtSettings.pathSelectorType == PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + Predictors.stateRewardPredictor = EngineAnalyticsContext.stateRewardPredictorFactory() + } + + logger.info { "PathSelectorType: ${UtSettings.pathSelectorType}" } + if (UtSettings.pathSelectorType == PathSelectorType.NN_REWARD_GUIDED_SELECTOR) { + logger.info { "RewardModelPath: ${UtSettings.rewardModelPath}" } + } // fix for CTRL-ALT-SHIFT-C from IDEA, which copies in class#method form // fix for path form @@ -333,7 +352,6 @@ fun runEstimator( if (updatedMethodFilter != null) logger.info { "Filtering: class='$classFqnFilter', method ='$methodNameFilter'" } - val projectToClassFQNs = classesLists.listFiles()!!.associate { it.name to File(it, "list").readLines() } val projects = mutableListOf() diff --git a/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimatorJdkPathProvider.kt b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimatorJdkPathProvider.kt new file mode 100644 index 0000000000..26f74eb32e --- /dev/null +++ b/utbot-junit-contest/src/main/kotlin/org/utbot/contest/ContestEstimatorJdkPathProvider.kt @@ -0,0 +1,14 @@ +package org.utbot.contest + +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.JdkPathDefaultProvider +import java.nio.file.Path + +/** + * This class is used to provide jdkPath set in [ContestEstimator] + * into the child process for concrete execution. + */ +class ContestEstimatorJdkPathProvider(private val path: String) : JdkPathDefaultProvider() { + override val jdkPath: Path + get() = path.toPath() +} \ No newline at end of file diff --git a/utbot-junit-contest/src/main/resources/classes/guava-30.0/list b/utbot-junit-contest/src/main/resources/classes/guava-30.0/list new file mode 100644 index 0000000000..8310e0eead --- /dev/null +++ b/utbot-junit-contest/src/main/resources/classes/guava-30.0/list @@ -0,0 +1 @@ +com.google.common.graph.StandardMutableValueGraph \ No newline at end of file diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/checker-qual-3.5.0.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/checker-qual-3.5.0.jar new file mode 100644 index 0000000000..f98cde8b2d Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/checker-qual-3.5.0.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/error_prone_annotations-2.3.4.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/error_prone_annotations-2.3.4.jar new file mode 100644 index 0000000000..c9bea2abc4 Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/error_prone_annotations-2.3.4.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/failureaccess-1.0.1.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/failureaccess-1.0.1.jar new file mode 100644 index 0000000000..9b56dc751c Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/failureaccess-1.0.1.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/guava-30.0-jre.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/guava-30.0-jre.jar new file mode 100644 index 0000000000..f3a7818479 Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/guava-30.0-jre.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/j2objc-annotations-1.3.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/j2objc-annotations-1.3.jar new file mode 100644 index 0000000000..a429c7219d Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/j2objc-annotations-1.3.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/jsr305-3.0.2.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/jsr305-3.0.2.jar new file mode 100644 index 0000000000..59222d9ca5 Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/jsr305-3.0.2.jar differ diff --git a/utbot-junit-contest/src/main/resources/projects/guava-30.0/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/utbot-junit-contest/src/main/resources/projects/guava-30.0/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar new file mode 100644 index 0000000000..45832c052a Binary files /dev/null and b/utbot-junit-contest/src/main/resources/projects/guava-30.0/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar differ diff --git a/utbot-maven/build.gradle b/utbot-maven/build.gradle new file mode 100644 index 0000000000..283153b563 --- /dev/null +++ b/utbot-maven/build.gradle @@ -0,0 +1,112 @@ +plugins { + id 'maven' +} + +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" + +configurations { + mavenEmbedder // it is used to run maven tasks from gradle +} + +dependencies { + // `compile` because `api` dependencies are not included in pom.xml by `install` task + compile project(':utbot-framework') + + implementation "org.apache.maven:maven-core:$maven_plugin_api_version" + implementation "org.apache.maven:maven-plugin-api:$maven_plugin_api_version" + compileOnly "org.apache.maven.plugin-tools:maven-plugin-annotations:$maven_plugin_tools_version" + implementation "io.github.microutils:kotlin-logging:$kotlin_logging_version" + + implementation "org.eclipse.sisu:org.eclipse.sisu.plexus:$sisu_plexus_version" + testImplementation "org.apache.maven.plugin-testing:maven-plugin-testing-harness:$maven_plugin_testing_version" + testImplementation "org.apache.maven:maven-compat:$maven_plugin_api_version" + testImplementation "org.apache.maven.resolver:maven-resolver-api:$maven_resolver_api_version" + + mavenEmbedder "org.apache.maven:maven-embedder:$maven_plugin_api_version" + mavenEmbedder "org.apache.maven:maven-compat:$maven_plugin_api_version" + mavenEmbedder "org.slf4j:slf4j-simple:$slf4j_version" + mavenEmbedder "org.eclipse.aether:aether-connector-basic:$eclipse_aether_version" + mavenEmbedder "org.eclipse.aether:aether-transport-wagon:$eclipse_aether_version" + mavenEmbedder "org.apache.maven.wagon:wagon-http:$maven_wagon_version" + mavenEmbedder "org.apache.maven.wagon:wagon-provider-api:$maven_wagon_version" +} + +/** + * We should run the maven task `install` to build & publish this plugin. + * But `utbot-maven` is the Gradle module (not Maven), so we have to + * manually generate the pom.xml file and the plugin descriptor file. + */ + +def buildDirectory = buildDir.canonicalPath +def outputDirectory = compileKotlin.destinationDir.canonicalPath +def pomFile = new File("$buildDir/pom.xml") +def pluginDescriptorFile = new File(outputDirectory, 'META-INF/maven/plugin.xml') + +/** + * Generates the pom.xml file and saves it to the [pomFile]. + */ +task generatePomFile(dependsOn: compileKotlin) { + outputs.file pomFile + + doLast { + install.repositories.mavenInstaller.pom.with { + groupId = project.group + artifactId = project.name + version = project.version + packaging = 'maven-plugin' + + withXml { + asNode().with { + appendNode('build').with { + appendNode('directory', buildDirectory) + appendNode('outputDirectory', outputDirectory) + } + def repositoriesNode = appendNode('repositories') + // `this.project` is the project from Gradle, but `project` is the project from Maven + this.project.repositories.indexed().forEach { index, repository -> + repositoriesNode.with { + appendNode('repository').with { + // `index` is needed for the uniqueness of the IDs + appendNode('id', "${repository.name}_${index}") + appendNode('url', repository.url) + } + } + } + } + } + } + install.repositories.mavenInstaller.pom.writeTo(pomFile) + + assert pomFile.file, "${pomFile.canonicalPath}: was not generated" + logger.info("POM is generated in ${pomFile.canonicalPath}") + } +} + +/** + * Generates the plugin descriptor file and saves it to the [pluginDescriptorFile]. + */ +task generatePluginDescriptor(type: JavaExec, dependsOn: generatePomFile) { + inputs.files project.compileKotlin.outputs.files + outputs.file pluginDescriptorFile + + workingDir projectDir + main = 'org.apache.maven.cli.MavenCli' + classpath = configurations.mavenEmbedder + systemProperties['maven.multiModuleProjectDirectory'] = projectDir + args = [ + '--errors', + '--batch-mode', + '--file', "${pomFile.path}", + 'org.apache.maven.plugins:maven-plugin-plugin:3.6.0:descriptor', + '-Dproject.build.sourceEncoding=UTF-8' + ] + + doLast { + assert pluginDescriptorFile.file, "${pluginDescriptorFile.canonicalPath}: was not generated" + logger.info("Plugin descriptor is generated in ${pluginDescriptorFile.canonicalPath}") + } +} + +project.install.dependsOn(generatePluginDescriptor) + +// Please, use `utbot-maven/other/install` task for publishing diff --git a/utbot-maven/docs/utbot-maven.md b/utbot-maven/docs/utbot-maven.md new file mode 100644 index 0000000000..d0949de8ca --- /dev/null +++ b/utbot-maven/docs/utbot-maven.md @@ -0,0 +1,143 @@ +## Utbot maven plugin + +Utbot Maven Plugin is a maven plugin for generating tests and creating SARIF-reports. + +The `generateTestsAndSarifReport` maven task generates tests and SARIF-reports for all classes in your project (or only for classes specified in the configuration). +In addition, it creates one big SARIF-report containing the results from all the processed files. + + +### How to use + +_TODO: The plugin has not been published yet._ + +- Apply the plugin: + ```XML + + + + org.utbot + utbot-maven + 1.0-SNAPSHOT + + + + ``` + +- Run maven task `utbot:generateTestsAndSarifReport` to create a report. + + +### How to configure + +For example, the following configuration may be used: + +```XML + + + com.abc.Main + com.qwerty.Util + + C:/.../SomeDirectory + target/generated/test + target/generated/sarif + true + junit5 + mockito + 60000L + java + other-packages + mock-statics + force + + org.slf4j.Logger + java.util.Random + + +``` + +**Note:** All configuration fields have default values, so there is no need to configure the plugin if you don't want to. + +**Description of fields:** +- `targetClasses` – + - Classes for which the SARIF-report will be created. + Uses all source classes from the user project if this list is empty. + - By default, an empty list is used. + +- `projectRoot` – + - **Absolute** path to the root of the relative paths in the SARIF-report. + - By default, the root of your project is used. + +- `generatedTestsRelativeRoot` – + - **Relative** path (against module root) to the root of the generated tests. + - By default, `'target/generated/test'` is used. + +- `sarifReportsRelativeRoot` – + - **Relative** path (against module root) to the root of the SARIF reports. + - By default, `'target/generated/sarif'` is used. + +- `markGeneratedTestsDirectoryAsTestSourcesRoot` – + - _TODO: It has not been supported yet._ + - Mark the directory with generated tests as `test sources root` or not. + - By default, `true` is used. + +- `testFramework` – + - The name of the test framework to be used. + - Can be one of: + - `'junit4'` + - `'junit5'` _(by default)_ + - `'testng'` + +- `mockFramework` – + - The name of the mock framework to be used. + - Can be one of: + - `'mockito'` _(by default)_ + +- `generationTimeout` – + - Time budget for generating tests for one class (in milliseconds). + - By default, 60 seconds is used. + +- `codegenLanguage` – + - The language of the generated tests. + - Can be one of: + - `'java'` _(by default)_ + - `'kotlin'` + +- `mockStrategy` – + - The mock strategy to be used. + - Can be one of: + - `'no-mocks'` – do not use mock frameworks at all + - `'other-packages'` – mock all classes outside the current package except system ones _(by default)_ + - `'other-classes'` – mock all classes outside the class under test except system ones + +- `staticsMocking` – + - Use static methods mocking or not. + - Can be one of: + - `'do-not-mock-statics'` + - `'mock-statics'` _(by default)_ + +- `forceStaticMocking` – + - Forces mocking static methods and constructors for `classesToMockAlways` classes or not. + - Can be one of: + - `'force'` _(by default)_ + - `'do-not-force'` + - **Note:** We force static mocking independently on this setting for some classes (e.g. `java.util.Random`). + +- `classesToMockAlways` – + - Classes to force mocking theirs static methods and constructors. + - By default, some internal classes are used. + + +### How to test + +If you want to change the source code of the plugin or even the whole utbot-project, +you need to do the following: +- Publish UTBot to the local maven repository using `utbot/publishToMavenLocal` gradle task. +- Publish `utbot-maven` to the local maven repository using `utbot-maven/other/install` gradle task. +- Add the plugin to your project (see the section __How to use__). + +### How to configure the log level + +To change the log level run the `generateTestsAndSarifReport` task with the appropriate flag. + +For example, `mvn utbot:generateTestsAndSarifReport --debug` + +Note that the internal maven log information will also be shown. diff --git a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojo.kt b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojo.kt new file mode 100644 index 0000000000..3728b39162 --- /dev/null +++ b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojo.kt @@ -0,0 +1,203 @@ +package org.utbot.maven.plugin + +import mu.KotlinLogging +import org.apache.maven.plugin.AbstractMojo +import org.apache.maven.plugins.annotations.Execute +import org.apache.maven.plugins.annotations.LifecyclePhase +import org.apache.maven.plugins.annotations.Mojo +import org.apache.maven.plugins.annotations.Parameter +import org.apache.maven.project.MavenProject +import org.utbot.common.bracket +import org.utbot.common.debug +import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.framework.plugin.api.util.withUtContext +import org.utbot.framework.plugin.sarif.GenerateTestsAndSarifReportFacade +import org.utbot.framework.plugin.sarif.TargetClassWrapper +import org.utbot.maven.plugin.extension.SarifMavenConfigurationProvider +import org.utbot.maven.plugin.wrappers.MavenProjectWrapper +import org.utbot.maven.plugin.wrappers.SourceFindingStrategyMaven +import java.io.File + +internal val logger = KotlinLogging.logger {} + +/** + * The main class containing the entry point [execute]. + * + * [Documentation](https://maven.apache.org/guides/plugin/guide-java-plugin-development.html) + */ +@Mojo( + name = "generateTestsAndSarifReport", + defaultPhase = LifecyclePhase.GENERATE_TEST_SOURCES +) +@Execute( + phase = LifecyclePhase.GENERATE_TEST_SOURCES +) +class GenerateTestsAndSarifReportMojo : AbstractMojo() { + + /** + * The maven project for which we are creating a SARIF report. + */ + @Parameter(defaultValue = "\${project}", readonly = true) + lateinit var mavenProject: MavenProject + + /** + * Classes for which the SARIF report will be created. + * Uses all classes from the user project if this list is empty. + */ + @Parameter(defaultValue = "") + internal var targetClasses: List = listOf() + + /** + * Absolute path to the root of the relative paths in the SARIF report. + */ + @Parameter(defaultValue = "\${project.basedir}") + internal lateinit var projectRoot: File + + /** + * Relative path (against module root) to the root of the generated tests. + */ + @Parameter(defaultValue = "target/generated/test") + internal lateinit var generatedTestsRelativeRoot: String + + /** + * Relative path (against module root) to the root of the SARIF reports. + */ + @Parameter(defaultValue = "target/generated/sarif") + internal lateinit var sarifReportsRelativeRoot: String + + /** + * Mark the directory with generated tests as `test sources root` or not. + */ + @Parameter(defaultValue = "true") + internal var markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean = true + + /** + * Can be one of: 'junit4', 'junit5', 'testng'. + */ + @Parameter(defaultValue = "junit5") + internal lateinit var testFramework: String + + /** + * Can be one of: 'mockito'. + */ + @Parameter(defaultValue = "mockito") + internal lateinit var mockFramework: String + + /** + * Maximum tests generation time for one class (in milliseconds). + */ + @Parameter + internal var generationTimeout: Long = 60 * 1000L + + /** + * Can be one of: 'java', 'kotlin'. + */ + @Parameter(defaultValue = "java") + internal lateinit var codegenLanguage: String + + /** + * Can be one of: 'no-mocks', 'other-packages', 'other-classes'. + */ + @Parameter(defaultValue = "no-mocks") + internal lateinit var mockStrategy: String + + /** + * Can be one of: 'do-not-mock-statics', 'mock-statics'. + */ + @Parameter(defaultValue = "do-not-mock-statics") + internal lateinit var staticsMocking: String + + /** + * Can be one of: 'force', 'do-not-force'. + */ + @Parameter(defaultValue = "force") + internal lateinit var forceStaticMocking: String + + /** + * Classes to force mocking theirs static methods and constructors. + */ + @Parameter(defaultValue = "") + internal var classesToMockAlways: List = listOf() + + /** + * Provides configuration needed to create a SARIF report. + */ + val sarifProperties: SarifMavenConfigurationProvider + get() = SarifMavenConfigurationProvider(this) + + /** + * Contains information about the maven project for which we are creating a SARIF report. + */ + lateinit var rootMavenProjectWrapper: MavenProjectWrapper + + /** + * Entry point: called when the user starts this maven task. + */ + override fun execute() { + try { + rootMavenProjectWrapper = MavenProjectWrapper(mavenProject, sarifProperties) + } catch (t: Throwable) { + logger.error(t) { "Unexpected error while configuring the maven task" } + return + } + try { + generateForProjectRecursively(rootMavenProjectWrapper) + GenerateTestsAndSarifReportFacade.mergeReports( + sarifReports = rootMavenProjectWrapper.collectReportsRecursively(), + mergedSarifReportFile = rootMavenProjectWrapper.sarifReportFile + ) + } catch (t: Throwable) { + logger.error(t) { "Unexpected error while generating SARIF report" } + return + } + } + + // internal + + /** + * Generates tests and a SARIF report for classes in the [mavenProjectWrapper] and in all its child projects. + */ + private fun generateForProjectRecursively(mavenProjectWrapper: MavenProjectWrapper) { + logger.debug().bracket("Generating tests for the '${mavenProjectWrapper.mavenProject.name}' source set") { + withUtContext(UtContext(mavenProjectWrapper.classLoader)) { + mavenProjectWrapper.targetClasses.forEach { targetClass -> + generateForClass(mavenProjectWrapper, targetClass) + } + } + } + mavenProjectWrapper.childProjects.forEach { childProject -> + generateForProjectRecursively(childProject) + } + } + + /** + * Generates tests and a SARIF report for the class [targetClass]. + */ + private fun generateForClass(mavenProjectWrapper: MavenProjectWrapper, targetClass: TargetClassWrapper) { + logger.debug().bracket("Generating tests for the $targetClass") { + val sourceFindingStrategy = + SourceFindingStrategyMaven(mavenProjectWrapper, targetClass.testsCodeFile.path) + val generateTestsAndSarifReportFacade = + GenerateTestsAndSarifReportFacade(sarifProperties, sourceFindingStrategy) + generateTestsAndSarifReportFacade.generateForClass( + targetClass, mavenProjectWrapper.workingDirectory, mavenProjectWrapper.runtimeClasspath + ) + } + } + + /** + * Returns SARIF reports created for this [MavenProjectWrapper] and for all its child projects. + */ + private fun MavenProjectWrapper.collectReportsRecursively(): List = + this.childProjects.flatMap { childProject -> + childProject.collectReportsRecursively() + } + this.collectReports() + + /** + * Returns SARIF reports created for this [MavenProjectWrapper]. + */ + private fun MavenProjectWrapper.collectReports(): List = + this.targetClasses.map { targetClass -> + targetClass.sarifReportFile.readText() + } +} diff --git a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProvider.kt b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProvider.kt new file mode 100644 index 0000000000..985308fc0a --- /dev/null +++ b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProvider.kt @@ -0,0 +1,59 @@ +package org.utbot.maven.plugin.extension + +import org.utbot.framework.codegen.ForceStaticMocking +import org.utbot.framework.codegen.StaticsMocking +import org.utbot.framework.codegen.TestFramework +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.framework.plugin.sarif.SarifExtensionProvider +import org.utbot.maven.plugin.GenerateTestsAndSarifReportMojo +import java.io.File + +/** + * Provides fields needed to create a SARIF report in a convenient form. + */ +class SarifMavenConfigurationProvider( + private val generateTestsAndSarifReportMojo: GenerateTestsAndSarifReportMojo +) : SarifExtensionProvider { + + override val targetClasses: List + get() = generateTestsAndSarifReportMojo.targetClasses + + override val projectRoot: File + get() = generateTestsAndSarifReportMojo.projectRoot + + override val generatedTestsRelativeRoot: String + get() = generateTestsAndSarifReportMojo.generatedTestsRelativeRoot + + override val sarifReportsRelativeRoot: String + get() = generateTestsAndSarifReportMojo.sarifReportsRelativeRoot + + override val markGeneratedTestsDirectoryAsTestSourcesRoot: Boolean + get() = generateTestsAndSarifReportMojo.markGeneratedTestsDirectoryAsTestSourcesRoot + + override val testFramework: TestFramework + get() = testFrameworkParse(generateTestsAndSarifReportMojo.testFramework) + + override val mockFramework: MockFramework + get() = mockFrameworkParse(generateTestsAndSarifReportMojo.mockFramework) + + override val generationTimeout: Long + get() = generationTimeoutParse(generateTestsAndSarifReportMojo.generationTimeout) + + override val codegenLanguage: CodegenLanguage + get() = codegenLanguageParse(generateTestsAndSarifReportMojo.codegenLanguage) + + override val mockStrategy: MockStrategyApi + get() = mockStrategyParse(generateTestsAndSarifReportMojo.mockStrategy) + + override val staticsMocking: StaticsMocking + get() = staticsMockingParse(generateTestsAndSarifReportMojo.staticsMocking) + + override val forceStaticMocking: ForceStaticMocking + get() = forceStaticMockingParse(generateTestsAndSarifReportMojo.forceStaticMocking) + + override val classesToMockAlways: Set + get() = classesToMockAlwaysParse(generateTestsAndSarifReportMojo.classesToMockAlways) +} \ No newline at end of file diff --git a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/MavenProjectWrapper.kt b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/MavenProjectWrapper.kt new file mode 100644 index 0000000000..bd72310590 --- /dev/null +++ b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/MavenProjectWrapper.kt @@ -0,0 +1,152 @@ +package org.utbot.maven.plugin.wrappers + +import org.apache.maven.project.MavenProject +import org.utbot.common.FileUtil.createNewFileWithParentDirectories +import org.utbot.common.FileUtil.findAllFilesOnly +import org.utbot.common.PathUtil +import org.utbot.common.PathUtil.toPath +import org.utbot.common.tryLoadClass +import org.utbot.framework.plugin.sarif.util.ClassUtil +import org.utbot.framework.plugin.sarif.TargetClassWrapper +import org.utbot.maven.plugin.extension.SarifMavenConfigurationProvider +import java.io.File +import java.net.URLClassLoader +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Contains information about the maven project for which we are creating a SARIF report. + */ +class MavenProjectWrapper( + val mavenProject: MavenProject, + val sarifProperties: SarifMavenConfigurationProvider +) { + + /** + * Contains child projects of the [mavenProject]. + */ + val childProjects: List by lazy { + mavenProject.collectedProjects.map { childProject -> + MavenProjectWrapper(childProject, sarifProperties) + } + } + + /** + * Directory for generated tests. For example, "build/generated/test/". + */ + val generatedTestsDirectory: File by lazy { + mavenProject.basedir.resolve(sarifProperties.generatedTestsRelativeRoot).apply { mkdirs() } + } + + /** + * Directory for created SARIF reports. For example, "build/generated/sarif/". + */ + val generatedSarifDirectory: File by lazy { + mavenProject.basedir.resolve(sarifProperties.sarifReportsRelativeRoot).apply { mkdirs() } + } + + /** + * SARIF report file containing results from all others reports from the [mavenProject]. + */ + val sarifReportFile: File by lazy { + Paths.get( + generatedSarifDirectory.path, + "${mavenProject.name}Report.sarif" + ).toFile().apply { + createNewFileWithParentDirectories() + } + } + + /** + * Runtime classpath of the [mavenProject]. + */ + val runtimeClasspath: String by lazy { + mavenProject.runtimeClasspathElements.joinToString(File.pathSeparator) + } + + /** + * ClassLoader that loaded classes from the [mavenProject]. + */ + val classLoader: URLClassLoader by lazy { + val urls = mavenProject.runtimeClasspathElements.map { path -> + path.toPath().toUri().toURL() + } + URLClassLoader(urls.toTypedArray()) + } + + /** + * Absolute path to the build directory. For example, "./target/classes". + */ + val workingDirectory: Path by lazy { + mavenProject.build.outputDirectory.toPath() + } + + /** + * List of declared in the [mavenProject] classes. + * Does not contain classes without declared methods or without a canonicalName. + */ + val targetClasses: List by lazy { + sarifProperties.targetClasses + .ifEmpty { + ClassUtil.findAllDeclaredClasses(classLoader, workingDirectory.toFile()) + } + .mapNotNull { classFqn -> + constructTargetClassWrapper(classFqn) + } + } + + /** + * Finds the source code file by the given class fully qualified name. + */ + fun findSourceCodeFile(classFqn: String): File? = + ClassUtil.findSourceCodeFile(classFqn, sourceCodeFiles, classLoader) + + // internal + + /** + * List of all source code files of this [mavenProject]. + */ + private val sourceCodeFiles: List by lazy { + mavenProject.compileSourceRoots.flatMap { srcDir -> + srcDir.toPath().toFile().findAllFilesOnly() + } + } + + /** + * Constructs [TargetClassWrapper] by the given [classFqn]. + */ + private fun constructTargetClassWrapper(classFqn: String): TargetClassWrapper? { + val classUnderTest = classLoader.tryLoadClass(classFqn)?.kotlin + ?: return null // `classFqn` may be defined not in this module + val sourceCodeFile = findSourceCodeFile(classFqn) + ?: error("The source code for the class $classFqn was not found") + return TargetClassWrapper( + classFqn, + classUnderTest, + sourceCodeFile, + createTestsCodeFile(classFqn), + createSarifReportFile(classFqn) + ) + } + + /** + * Creates and returns a file for a future SARIF report. + * For example, ".../main/com/qwerty/MainReport.sarif". + */ + private fun createSarifReportFile(classFqn: String): File { + val relativePath = "${PathUtil.classFqnToPath(classFqn)}Report.sarif" + val absolutePath = Paths.get(generatedSarifDirectory.path, relativePath) + return absolutePath.toFile().apply { createNewFileWithParentDirectories() } + } + + /** + * Creates and returns a file for future generated tests. + * For example, ".../com/qwerty/MainTest.java". + */ + private fun createTestsCodeFile(classFqn: String): File { + val fileExtension = sarifProperties.codegenLanguage.extension + val relativePath = "${PathUtil.classFqnToPath(classFqn)}Test$fileExtension" + val absolutePath = Paths.get(generatedTestsDirectory.path, relativePath) + return absolutePath.toFile().apply { createNewFileWithParentDirectories() } + } +} \ No newline at end of file diff --git a/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt new file mode 100644 index 0000000000..515aad43be --- /dev/null +++ b/utbot-maven/src/main/kotlin/org/utbot/maven/plugin/wrappers/SourceFindingStrategyMaven.kt @@ -0,0 +1,43 @@ +package org.utbot.maven.plugin.wrappers + +import org.utbot.common.PathUtil +import org.utbot.common.PathUtil.toPath +import org.utbot.sarif.SourceFindingStrategy + +/** + * The search strategy based on the information available to the Maven. + * + * @param mavenProjectWrapper the maven project wrapper which contains all needed source files. + * @param testsFilePath absolute path to the file containing generated tests + * for the class for which we are creating the SARIF report. + * @param defaultExtension the file extension to be used in [getSourceRelativePath] + * if the source file was not found by the class qualified name + * and the `extension` parameter is null. + */ +class SourceFindingStrategyMaven( + private val mavenProjectWrapper: MavenProjectWrapper, + private val testsFilePath: String, + private val defaultExtension: String = ".java" +) : SourceFindingStrategy() { + + /** + * Returns the relative path (against [projectRootPath]) to the file with generated tests. + */ + override val testsRelativePath: String + get() = PathUtil.safeRelativize(projectRootPath, testsFilePath) + ?: testsFilePath.toPath().fileName.toString() + + /** + * Returns the relative path (against [projectRootPath]) to the source file containing the class [classFqn]. + */ + override fun getSourceRelativePath(classFqn: String, extension: String?): String { + val defaultPath = PathUtil.classFqnToPath(classFqn) + (extension ?: defaultExtension) + return mavenProjectWrapper.findSourceCodeFile(classFqn)?.let { sourceCodeFile -> + PathUtil.safeRelativize(projectRootPath, sourceCodeFile.absolutePath) + } ?: defaultPath + } + + // internal + + private val projectRootPath = mavenProjectWrapper.sarifProperties.projectRoot.absolutePath +} \ No newline at end of file diff --git a/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojoTest.kt b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojoTest.kt new file mode 100644 index 0000000000..c2ff1de7fb --- /dev/null +++ b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/GenerateTestsAndSarifReportMojoTest.kt @@ -0,0 +1,73 @@ +package org.utbot.maven.plugin + +import org.apache.maven.plugin.testing.AbstractMojoTestCase +import org.apache.maven.project.MavenProject +import org.junit.jupiter.api.* +import org.utbot.common.PathUtil.toPath +import org.utbot.framework.util.GeneratedSarif +import java.io.File + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GenerateTestsAndSarifReportMojoTest : AbstractMojoTestCase() { + + @BeforeAll + override fun setUp() { + super.setUp() + } + + @AfterAll + override fun tearDown() { + super.tearDown() + } + + @Test + fun `test directory exists and not empty`() { + val testsRelativePath = sarifReportMojo.sarifProperties.generatedTestsRelativeRoot + val testDirectory = testMavenProject.projectBaseDir.resolve(testsRelativePath) + assert(directoryExistsAndNotEmpty(testDirectory)) + } + + @Test + fun `sarif directory exists and not empty`() { + val reportsRelativePath = sarifReportMojo.sarifProperties.sarifReportsRelativeRoot + val sarifDirectory = testMavenProject.projectBaseDir.resolve(reportsRelativePath) + assert(directoryExistsAndNotEmpty(sarifDirectory)) + } + + @Test + fun `sarif report contains all required results`() { + val sarifReportFile = sarifReportMojo.rootMavenProjectWrapper.sarifReportFile + val sarifReportText = sarifReportFile.readText() + GeneratedSarif(sarifReportText).apply { + assert(hasSchema()) + assert(hasVersion()) + assert(hasRules()) + assert(hasResults()) + assert(hasCodeFlows()) + assert(codeFlowsIsNotEmpty()) + assert(contains("ArithmeticException")) + } + } + + // internal + + private val testMavenProject: TestMavenProject = + TestMavenProject("src/test/resources/project-to-test".toPath()) + + private val sarifReportMojo by lazy { + configureSarifReportMojo(testMavenProject.mavenProject).apply { + this.execute() + } + } + + private fun configureSarifReportMojo(mavenProject: MavenProject): GenerateTestsAndSarifReportMojo { + val generateTestsAndSarifReportMojo = configureMojo( + GenerateTestsAndSarifReportMojo(), "utbot-maven", mavenProject.file + ) as GenerateTestsAndSarifReportMojo + generateTestsAndSarifReportMojo.mavenProject = mavenProject + return generateTestsAndSarifReportMojo + } + + private fun directoryExistsAndNotEmpty(directory: File): Boolean = + directory.exists() && directory.isDirectory && directory.list()?.isNotEmpty() == true +} \ No newline at end of file diff --git a/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/TestMavenProject.kt b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/TestMavenProject.kt new file mode 100644 index 0000000000..6c6bac1ddb --- /dev/null +++ b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/TestMavenProject.kt @@ -0,0 +1,38 @@ +package org.utbot.maven.plugin + +import org.apache.commons.lang3.reflect.FieldUtils +import org.apache.maven.model.io.xpp3.MavenXpp3Reader +import org.apache.maven.project.MavenProject +import org.codehaus.plexus.util.FileUtils +import java.io.File +import java.io.FileReader +import java.nio.file.Path + +/** + * Wrapper for the maven project stored in the test resources. + */ +class TestMavenProject(pathToProject: Path) { + + /** + * Path to the copied maven project. + */ + val projectBaseDir = + File("build/resources/${pathToProject.fileName}") + + init { + projectBaseDir.deleteRecursively() + // copying the project to the build directory to change it there + FileUtils.copyDirectoryStructure(pathToProject.toFile(), projectBaseDir) + } + + val mavenProject: MavenProject = run { + val pomFile = File(projectBaseDir, "pom.xml") + val model = MavenXpp3Reader().read(FileReader(pomFile)) + val mavenProject = MavenProject(model) + mavenProject.setPomFile(pomFile) + mavenProject.collectedProjects = listOf() // no child modules + mavenProject.addCompileSourceRoot(mavenProject.build.sourceDirectory) + FieldUtils.writeField(mavenProject, "basedir", projectBaseDir, true) + mavenProject + } +} \ No newline at end of file diff --git a/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProviderTest.kt b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProviderTest.kt new file mode 100644 index 0000000000..a9bcc6317f --- /dev/null +++ b/utbot-maven/src/test/kotlin/org/utbot/maven/plugin/extension/SarifMavenConfigurationProviderTest.kt @@ -0,0 +1,119 @@ +package org.utbot.maven.plugin.extension + +import org.apache.maven.plugin.testing.AbstractMojoTestCase +import org.apache.maven.project.MavenProject +import org.junit.jupiter.api.* +import org.utbot.common.PathUtil.toPath +import org.utbot.engine.Mocker +import org.utbot.framework.codegen.* +import org.utbot.framework.plugin.api.ClassId +import org.utbot.framework.plugin.api.CodegenLanguage +import org.utbot.framework.plugin.api.MockFramework +import org.utbot.framework.plugin.api.MockStrategyApi +import org.utbot.maven.plugin.GenerateTestsAndSarifReportMojo +import org.utbot.maven.plugin.TestMavenProject +import java.io.File + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SarifMavenConfigurationProviderTest : AbstractMojoTestCase() { + + @BeforeAll + override fun setUp() { + super.setUp() + } + + @AfterAll + override fun tearDown() { + super.tearDown() + } + + @Test + fun `targetClasses should be provided from the configuration`() { + Assertions.assertEquals(listOf("Main"), configurationProvider.targetClasses) + } + + @Test + fun `projectRoot should be provided from the configuration`() { + Assertions.assertEquals(File("build/resources/project-to-test"), configurationProvider.projectRoot) + } + + @Test + fun `generatedTestsRelativeRoot should be provided from the configuration`() { + Assertions.assertEquals("target/generated/test", configurationProvider.generatedTestsRelativeRoot) + } + + @Test + fun `sarifReportsRelativeRoot should be provided from the configuration`() { + Assertions.assertEquals("target/generated/sarif", configurationProvider.sarifReportsRelativeRoot) + } + + @Test + fun `markGeneratedTestsDirectoryAsTestSourcesRoot should be provided from the configuration`() { + Assertions.assertEquals(true, configurationProvider.markGeneratedTestsDirectoryAsTestSourcesRoot) + } + + @Test + fun `testFramework should be provided from the configuration`() { + Assertions.assertEquals(Junit5, configurationProvider.testFramework) + } + + + @Test + fun `mockFramework should be provided from the configuration`() { + Assertions.assertEquals(MockFramework.MOCKITO, configurationProvider.mockFramework) + } + + + @Test + fun `generationTimeout should be provided from the configuration`() { + Assertions.assertEquals(10000, configurationProvider.generationTimeout) + } + + @Test + fun `codegenLanguage should be provided from the configuration`() { + Assertions.assertEquals(CodegenLanguage.JAVA, configurationProvider.codegenLanguage) + } + + @Test + fun `mockStrategy should be provided from the configuration`() { + Assertions.assertEquals(MockStrategyApi.OTHER_PACKAGES, configurationProvider.mockStrategy) + } + + @Test + fun `staticsMocking should be provided from the configuration`() { + Assertions.assertEquals(MockitoStaticMocking, configurationProvider.staticsMocking) + } + + @Test + fun `forceStaticMocking should be provided from the configuration`() { + Assertions.assertEquals(ForceStaticMocking.FORCE, configurationProvider.forceStaticMocking) + } + + @Test + fun `classesToMockAlways should be provided from the configuration`() { + val expectedClassesToMockAlways = + (Mocker.defaultSuperClassesToMockAlwaysNames + "java.io.File").map(::ClassId).toSet() + Assertions.assertEquals(expectedClassesToMockAlways, configurationProvider.classesToMockAlways) + } + + // internal + + private val testMavenProject: TestMavenProject = + TestMavenProject("src/test/resources/project-to-test".toPath()) + + private val sarifReportMojo by lazy { + configureSarifReportMojo(testMavenProject.mavenProject) + } + + private val configurationProvider by lazy { + sarifReportMojo.sarifProperties + } + + private fun configureSarifReportMojo(mavenProject: MavenProject): GenerateTestsAndSarifReportMojo { + val generateTestsAndSarifReportMojo = configureMojo( + GenerateTestsAndSarifReportMojo(), "utbot-maven", mavenProject.file + ) as GenerateTestsAndSarifReportMojo + generateTestsAndSarifReportMojo.mavenProject = mavenProject + return generateTestsAndSarifReportMojo + } +} \ No newline at end of file diff --git a/utbot-maven/src/test/resources/project-to-test/pom.xml b/utbot-maven/src/test/resources/project-to-test/pom.xml new file mode 100644 index 0000000000..e5e3f239b7 --- /dev/null +++ b/utbot-maven/src/test/resources/project-to-test/pom.xml @@ -0,0 +1,47 @@ + + + 4.0.0 + org.example + project-to-test + 1.0 + + + 8 + 8 + + + + + build/resources/project-to-test/src/main/java + build/resources/project-to-test/target/classes + + + + org.utbot + utbot-maven + 1.0-SNAPSHOT + + + Main + + build/resources/project-to-test + target/generated/test + target/generated/sarif + true + junit5 + mockito + 10000 + java + other-packages + mock-statics + force + + java.io.File + + + + + + \ No newline at end of file diff --git a/utbot-maven/src/test/resources/project-to-test/src/main/java/Main.java b/utbot-maven/src/test/resources/project-to-test/src/main/java/Main.java new file mode 100644 index 0000000000..3e0dea0fc4 --- /dev/null +++ b/utbot-maven/src/test/resources/project-to-test/src/main/java/Main.java @@ -0,0 +1,5 @@ +public class Main { + public int f(int a) { + return 1 / a; + } +} diff --git a/utbot-maven/src/test/resources/project-to-test/target/classes/Main.class b/utbot-maven/src/test/resources/project-to-test/target/classes/Main.class new file mode 100644 index 0000000000..ae1e4ec1ea Binary files /dev/null and b/utbot-maven/src/test/resources/project-to-test/target/classes/Main.class differ diff --git a/utbot-sample/build.gradle b/utbot-sample/build.gradle index 554fc925fd..7a5d55d38b 100644 --- a/utbot-sample/build.gradle +++ b/utbot-sample/build.gradle @@ -12,10 +12,10 @@ dependencies { implementation group: 'javax.validation', name: 'validation-api', version: '2.0.0.Final' implementation group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' -// To use JUnit5, comment out junit4 and uncomment JUnit5 dependencies here. Please also check "test" section - testImplementation group: 'junit', name: 'junit', version: '4.13.1' -// testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.7.0' -// testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' +// To use JUnit4, comment out JUnit5 and uncomment JUnit4 dependencies here. Please also check "test" section +// testImplementation group: 'junit', name: 'junit', version: '4.13.1' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-params', version: '5.7.0' + testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: '5.7.0' // testImplementation group: 'org.mockito', name: 'mockito-core', version: '3.5.13' @@ -29,7 +29,7 @@ java { } test { -// To use JUnit5, comment out useJUnit and uncomment useJUnitPlatform. Please also check "dependencies" section - useJUnit() - // useJUnitPlatform() +// To use JUnit4, comment out useJUnitPlatform and uncomment useJUnit. Please also check "dependencies" section + //useJUnit() + useJUnitPlatform() } \ No newline at end of file diff --git a/utbot-sample/settings.gradle b/utbot-sample/settings.gradle deleted file mode 100644 index abdd2f2f43..0000000000 --- a/utbot-sample/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'utbot-sample' \ No newline at end of file diff --git a/utbot-sample/src/main/java/guava/examples/math/Stats.java b/utbot-sample/src/main/java/guava/examples/math/Stats.java new file mode 100644 index 0000000000..a99a37fbda --- /dev/null +++ b/utbot-sample/src/main/java/guava/examples/math/Stats.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package guava.examples.math; + +import java.io.Serializable; +import java.util.Iterator; + + +public final class Stats implements Serializable { + + private final long count; + private final double mean; + private final double sumOfSquaresOfDeltas; + private final double min; + private final double max; + + /** + * Internal constructor. Users should use {@link #ofIterable} or {@link StatsAccumulator#snapshot}. + * + *

    To ensure that the created instance obeys its contract, the parameters should satisfy the + * following constraints. This is the callers responsibility and is not enforced here. + * + *

      + *
    • If {@code count} is 0, {@code mean} may have any finite value (its only usage will be to + * get multiplied by 0 to calculate the sum), and the other parameters may have any values + * (they will not be used). + *
    • If {@code count} is 1, {@code sumOfSquaresOfDeltas} must be exactly 0.0 or {@link + * Double#NaN}. + *
    + */ + Stats(long count, double mean, double sumOfSquaresOfDeltas, double min, double max) { + this.count = count; + this.mean = mean; + this.sumOfSquaresOfDeltas = sumOfSquaresOfDeltas; + this.min = min; + this.max = max; + } + + /** + * Returns statistics over a dataset containing the given values. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision) + */ + public static Stats ofIterable(Iterable values) { + StatsAccumulator accumulator = new StatsAccumulator(); + accumulator.addAll(values); + return accumulator.snapshot(); + } + + /** + * Returns statistics over a dataset containing the given values. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision) + */ + public static Stats ofIterator(Iterator values) { + StatsAccumulator accumulator = new StatsAccumulator(); + accumulator.addAll(values); + return accumulator.snapshot(); + } + + /** + * Returns statistics over a dataset containing the given values. + * + * @param values a series of values + */ + public static Stats ofDoubles(double... values) { + StatsAccumulator acummulator = new StatsAccumulator(); + acummulator.addAll(values); + return acummulator.snapshot(); + } + + /** + * Returns statistics over a dataset containing the given values. + * + * @param values a series of values + */ + public static Stats ofInts(int... values) { + StatsAccumulator acummulator = new StatsAccumulator(); + acummulator.addAll(values); + return acummulator.snapshot(); + } + + /** + * Returns statistics over a dataset containing the given values. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision for longs of magnitude over 2^53 (slightly over 9e15)) + */ + public static Stats ofLongs(long... values) { + StatsAccumulator acummulator = new StatsAccumulator(); + acummulator.addAll(values); + return acummulator.snapshot(); + } + + /** Returns the number of values. */ + public long count() { + return count; + } + + + private static final long serialVersionUID = 0; +} diff --git a/utbot-sample/src/main/java/guava/examples/math/StatsAccumulator.java b/utbot-sample/src/main/java/guava/examples/math/StatsAccumulator.java new file mode 100644 index 0000000000..57c9e93381 --- /dev/null +++ b/utbot-sample/src/main/java/guava/examples/math/StatsAccumulator.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2012 The Guava Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ + +package guava.examples.math; + +import java.util.Iterator; + +import static java.lang.Double.*; + +public final class StatsAccumulator { + + // These fields must satisfy the requirements of Stats' constructor as well as those of the stat + // methods of this class. + private long count = 0; + private double mean = 0.0; // any finite value will do, we only use it to multiply by zero for sum + private double sumOfSquaresOfDeltas = 0.0; + private double min = NaN; // any value will do + private double max = NaN; // any value will do + + /** Adds the given value to the dataset. */ + public void add(double value) { + if (count == 0) { + count = 1; + mean = value; + min = value; + max = value; + if (!isFinite(value)) { + sumOfSquaresOfDeltas = NaN; + } + } else { + count++; + if (isFinite(value) && isFinite(mean)) { + // Art of Computer Programming vol. 2, Knuth, 4.2.2, (15) and (16) + double delta = value - mean; + mean += delta / count; + sumOfSquaresOfDeltas += delta * (value - mean); + } else { + mean = calculateNewMeanNonFinite(mean, value); + sumOfSquaresOfDeltas = NaN; + } + min = Math.min(min, value); + max = Math.max(max, value); + } + } + + /** + * Adds the given values to the dataset. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision) + */ + public void addAll(Iterable values) { + for (Number value : values) { + add(value.doubleValue()); + } + } + + /** + * Adds the given values to the dataset. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision) + */ + public void addAll(Iterator values) { + while (values.hasNext()) { + add(values.next().doubleValue()); + } + } + + /** + * Adds the given values to the dataset. + * + * @param values a series of values + */ + public void addAll(double... values) { + for (double value : values) { + add(value); + } + } + + /** + * Adds the given values to the dataset. + * + * @param values a series of values + */ + public void addAll(int... values) { + for (int value : values) { + add(value); + } + } + + /** + * Adds the given values to the dataset. + * + * @param values a series of values, which will be converted to {@code double} values (this may + * cause loss of precision for longs of magnitude over 2^53 (slightly over 9e15)) + */ + public void addAll(long... values) { + for (long value : values) { + add(value); + } + } + + /** Returns an immutable snapshot of the current statistics. */ + public Stats snapshot() { + return new Stats(count, mean, sumOfSquaresOfDeltas, min, max); + } + + /** Returns the number of values. */ + public long count() { + return count; + } + + /** + * Calculates the new value for the accumulated mean when a value is added, in the case where at + * least one of the previous mean and the value is non-finite. + */ + static double calculateNewMeanNonFinite(double previousMean, double value) { + /* + * Desired behaviour is to match the results of applying the naive mean formula. In particular, + * the update formula can subtract infinities in cases where the naive formula would add them. + * + * Consequently: + * 1. If the previous mean is finite and the new value is non-finite then the new mean is that + * value (whether it is NaN or infinity). + * 2. If the new value is finite and the previous mean is non-finite then the mean is unchanged + * (whether it is NaN or infinity). + * 3. If both the previous mean and the new value are non-finite and... + * 3a. ...either or both is NaN (so mean != value) then the new mean is NaN. + * 3b. ...they are both the same infinities (so mean == value) then the mean is unchanged. + * 3c. ...they are different infinities (so mean != value) then the new mean is NaN. + */ + if (isFinite(previousMean)) { + // This is case 1. + return value; + } else if (isFinite(value) || previousMean == value) { + // This is case 2. or 3b. + return previousMean; + } else { + // This is case 3a. or 3c. + return NaN; + } + } + + public static boolean isFinite(double value) { + return NEGATIVE_INFINITY < value && value < POSITIVE_INFINITY; + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/arrays/ArrayOfArrays.java b/utbot-sample/src/main/java/org/utbot/examples/arrays/ArrayOfArrays.java index 4615410654..c4ec038c30 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/arrays/ArrayOfArrays.java +++ b/utbot-sample/src/main/java/org/utbot/examples/arrays/ArrayOfArrays.java @@ -152,4 +152,14 @@ public int[][] fillMultiArrayWithArray(int[] value) { return array; } + + public Object[] arrayWithItselfAnAsElement(Object[] array) { + UtMock.assume(array != null && array.length > 0); + + if (array[0] == array) { + return array; + } + + return null; + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/arrays/IntArrayBasics.java b/utbot-sample/src/main/java/org/utbot/examples/arrays/IntArrayBasics.java index 5936e71d94..9a95e84200 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/arrays/IntArrayBasics.java +++ b/utbot-sample/src/main/java/org/utbot/examples/arrays/IntArrayBasics.java @@ -1,8 +1,28 @@ package org.utbot.examples.arrays; +import org.utbot.api.mock.UtMock; + import java.util.Arrays; public class IntArrayBasics { + public int[] intArrayWithAssumeOrExecuteConcretely(int x, int n) { + UtMock.assumeOrExecuteConcretely(n > 30); + + if (x > 0) { + if (n < 20) { + return new int[2]; + } else { + return new int[4]; + } + } else { + if (n < 20) { + return new int[10]; + } else { + return new int[20]; + } + } + } + public int[] initAnArray(int n) { int[] a = new int[n]; a[n - 1] = n - 1; diff --git a/utbot-sample/src/main/java/org/utbot/examples/collections/Lists.java b/utbot-sample/src/main/java/org/utbot/examples/collections/Lists.java index d81f3583de..3c66dd0c69 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/collections/Lists.java +++ b/utbot-sample/src/main/java/org/utbot/examples/collections/Lists.java @@ -1,5 +1,7 @@ package org.utbot.examples.collections; +import org.utbot.api.mock.UtMock; + import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; @@ -8,6 +10,11 @@ import java.util.Objects; public class Lists { + int bigListFromParameters(List list) { + UtMock.assume(list != null && list.size() == 11); + + return list.size(); + } Collection getNonEmptyCollection(Collection collection) { if (collection.size() == 0) { diff --git a/utbot-sample/src/main/java/org/utbot/examples/collections/Optionals.java b/utbot-sample/src/main/java/org/utbot/examples/collections/Optionals.java index f99b4cc7c1..405e95ba23 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/collections/Optionals.java +++ b/utbot-sample/src/main/java/org/utbot/examples/collections/Optionals.java @@ -314,4 +314,8 @@ public boolean equalOptionalsDouble(OptionalDouble left, OptionalDouble right) { return false; } } + + public Optional optionalOfPositive(int value) { + return value > 0 ? Optional.of(value) : Optional.empty(); + } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnum.java b/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnum.java index 4c3fe80aa5..14a0ded3fc 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnum.java +++ b/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnum.java @@ -1,37 +1,241 @@ package org.utbot.examples.enums; -import static org.utbot.examples.enums.ClassWithEnum.StatusEnum.*; +import static org.utbot.examples.enums.ClassWithEnum.ManyConstantsEnum.A; +import static org.utbot.examples.enums.ClassWithEnum.ManyConstantsEnum.B; +import static org.utbot.examples.enums.ClassWithEnum.StatusEnum.ERROR; +import static org.utbot.examples.enums.ClassWithEnum.StatusEnum.READY; +@SuppressWarnings({"UnnecessaryLocalVariable", "IfStatementWithIdenticalBranches"}) public class ClassWithEnum { public int useOrdinal(String s) { if (s != null) { - return READY.ordinal(); + final int ordinal = READY.ordinal(); + return ordinal; } else { - return ERROR.ordinal(); + final int ordinal = ERROR.ordinal(); + return ordinal; } } - @SuppressWarnings("unused") public int useGetter(String s) { if (s != null) { - return READY.getX(); + final int mutableInt = READY.getMutableInt(); + return mutableInt; + } else { + final int mutableInt = ERROR.getMutableInt(); + return mutableInt; + } + } + + @SuppressWarnings("UnnecessaryLocalVariable") + public int useEnumInDifficultIf(String s) { + if ("TRYIF".equalsIgnoreCase(s)) { + final ManyConstantsEnum[] values = ManyConstantsEnum.values(); + return foo(values[0]); } else { - return ERROR.getX(); + final ManyConstantsEnum b = B; + return foo(b); + } + } + + private int foo(ManyConstantsEnum e) { + if (e.equals(A)) { + return 1; + } else { + return 2; + } + } + + public int nullEnumAsParameter(StatusEnum statusEnum) { + final int ordinal = statusEnum.ordinal(); + return ordinal; + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + public int nullField(StatusEnum statusEnum) { + // catch NPE + statusEnum.s.length(); + + statusEnum.s = "404"; + return statusEnum.s.length(); + } + + public int changeEnum(StatusEnum statusEnum) { + if (statusEnum == READY) { + statusEnum = ERROR; + } else { + statusEnum = READY; + } + + return statusEnum.ordinal(); + } + + public String checkName(String s) { + final String name = READY.name(); + if (s.equals(name)) { + return ERROR.name(); + } + + return READY.name(); + } + + public int changeMutableField(StatusEnum statusEnum) { + if (statusEnum == READY) { + READY.mutableInt = 2; + + return READY.mutableInt; } + + ERROR.mutableInt = -2; + return ERROR.mutableInt; + } + + @SuppressWarnings("unused") + public boolean changingStaticWithEnumInit() { + // run and sections + final EnumWithStaticAffectingInit[] values = EnumWithStaticAffectingInit.values(); + + return true; } enum StatusEnum { - READY(0), - ERROR(-1); + READY(0, 10, "200"), + ERROR(-1, -10, null); + + int mutableInt; + final int code; + String s; + + StatusEnum(int mutableInt, final int code, String s) { + this.mutableInt = mutableInt; + this.code = code; + this.s = s; + } + + public int getMutableInt() { + return mutableInt; + } + + public int getCode() { + return code; + } + + static StatusEnum fromCode(int code) { + for (StatusEnum value : values()) { + if (value.getCode() == code) { + return value; + } + } + + throw new IllegalArgumentException("No enum corresponding to given code: " + code); + } + + static StatusEnum fromIsReady(boolean isReady) { + return isReady ? READY : ERROR; + } + + int publicGetCode() { + return this == READY ? 10 : -10; + } + } + + enum ManyConstantsEnum { + A, B, C, D, E, F, G, H, I, J, K + } + + static int x = 0; + + static class ClassWithStaticField { + static int y = 0; + + static void increment() { + y++; + } + } + + enum EnumWithStaticAffectingInit { + A, B; + + EnumWithStaticAffectingInit() { + ClassWithStaticField.y++; + ClassWithStaticField.increment(); + invokeIncrement(); + invokeIncrementStatic(); + + // changes after all init sections: + // y = y + 4 * 2 = y + 8 + } + + static { + x++; + ClassWithStaticField.y++; + ClassWithStaticField.increment(); + invokeIncrementStatic(); + + // changes after clinit section: + // y = y + 3 + // x = x + 1 + } + + void invokeIncrement() { + ClassWithStaticField.increment(); + } + + static void invokeIncrementStatic() { + ClassWithStaticField.increment(); + } + } + + public int implementingInterfaceEnumInDifficultBranch(String s) { + if ("SUCCESS".equalsIgnoreCase(s)) { + return EnumImplementingInterface.x + EnumImplementingInterface.A_INHERITOR.ordinal(); + } else { + return EnumImplementingInterface.y + EnumImplementingInterface.B_INHERITOR.ordinal(); + } + } + + interface AncestorInterface { + int y = 1; + } + + interface InterfaceWithField extends AncestorInterface { + int x = 0; + } + + enum EnumImplementingInterface implements InterfaceWithField { + A_INHERITOR, B_INHERITOR, C_INHERITOR, D_INHERITOR, + E_INHERITOR, F_INHERITOR, G_INHERITOR, H_INHERITOR, + I_INHERITOR, J_INHERITOR, K_INHERITOR, L_INHERITOR, + M_INHERITOR, N_INHERITOR, O_INHERITOR, P_INHERITOR, + } + + boolean affectSystemStaticAndInitEnumFromItAndReturnField() { + int prevStaticValue = ClassWithEnum.staticInt; + staticInt++; + + return OuterStaticUsageEnum.A.y != prevStaticValue; + } + + boolean affectSystemStaticAndInitEnumFromItAndGetItFromEnumFun() { + int prevStaticValue = ClassWithEnum.staticInt; + staticInt++; + + return OuterStaticUsageEnum.A.getOuterStatic() != prevStaticValue; + } + + static int staticInt = 0; + + enum OuterStaticUsageEnum { + A; - int x; + int y; - StatusEnum(int x) { - this.x = x; + OuterStaticUsageEnum() { + y = staticInt; } - public int getX() { - return x; + int getOuterStatic() { + return staticInt; } } } diff --git a/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranches.java b/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranches.java deleted file mode 100644 index 76bfaeb37e..0000000000 --- a/utbot-sample/src/main/java/org/utbot/examples/enums/ClassWithEnumInsideDifficultBranches.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.utbot.examples.enums; - -public class ClassWithEnumInsideDifficultBranches { - public int useEnumInDifficultIf(String s) { - if ("TRYIF".equalsIgnoreCase(s)) { - return foo(ManyConstantsEnum.A); - } else { - return foo(ManyConstantsEnum.B); - } - } - - private int foo(ManyConstantsEnum e) { - if (e.equals(ManyConstantsEnum.A)) { - return 1; - } else { - return 2; - } - } - - @SuppressWarnings("unused") - enum ManyConstantsEnum { - A, B, C, D, E, F, G, H, I, J, K - } -} diff --git a/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionClusteringExamples.java b/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionClusteringExamples.java index 0f8dea29c4..1142a9a685 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionClusteringExamples.java +++ b/utbot-sample/src/main/java/org/utbot/examples/exceptions/ExceptionClusteringExamples.java @@ -20,7 +20,7 @@ public int differentExceptionsInNestedCall(int i) throws MyCheckedException { } public int sleepingMoreThanDefaultTimeout(int i) throws InterruptedException { - Thread.sleep(500L); + Thread.sleep(1500L); if (i < 0) { throw new RuntimeException(); diff --git a/utbot-sample/src/main/java/org/utbot/examples/lambda/SimpleLambdaExamples.java b/utbot-sample/src/main/java/org/utbot/examples/lambda/SimpleLambdaExamples.java new file mode 100644 index 0000000000..c74a5f60dd --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/lambda/SimpleLambdaExamples.java @@ -0,0 +1,21 @@ +package org.utbot.examples.lambda; + +import java.util.function.BiFunction; +import java.util.function.Predicate; + +public class SimpleLambdaExamples { + public int biFunctionLambdaExample(int a, int b) { + BiFunction division = (numerator, divisor) -> numerator / divisor; + + return division.apply(a, b); + } + + @SuppressWarnings("Convert2MethodRef") + public Predicate choosePredicate(boolean isNotNullPredicate) { + if (isNotNullPredicate) { + return (o -> o != null); + } else { + return (o -> o == null); + } + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/objects/SimpleEnum.java b/utbot-sample/src/main/java/org/utbot/examples/objects/SimpleEnum.java deleted file mode 100644 index f8b3ca4163..0000000000 --- a/utbot-sample/src/main/java/org/utbot/examples/objects/SimpleEnum.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.utbot.examples.objects; - -public enum SimpleEnum { - FIRST(1), - SECOND(2); - - private final int code; - - SimpleEnum(int code) { - this.code = code; - } - - static SimpleEnum fromCode(int code) { - for (SimpleEnum value : values()) { - if (value.getCode() == code) { - return value; - } - } - throw new IllegalArgumentException("No enum corresponding to given code: " + code); - } - - private int getCode() { - return code; - } - - static SimpleEnum fromIsFirst(boolean isFirst) { - return isFirst ? FIRST : SECOND; - } - - int publicGetCode() { - return this == FIRST ? 1 : 2; - } -} \ No newline at end of file diff --git a/utbot-sample/src/main/java/org/utbot/examples/stream/BaseStreamExample.java b/utbot-sample/src/main/java/org/utbot/examples/stream/BaseStreamExample.java new file mode 100644 index 0000000000..326f3b12b9 --- /dev/null +++ b/utbot-sample/src/main/java/org/utbot/examples/stream/BaseStreamExample.java @@ -0,0 +1,604 @@ +package org.utbot.examples.stream; + +import org.jetbrains.annotations.NotNull; +import org.utbot.api.mock.UtMock; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@SuppressWarnings({"IfStatementWithIdenticalBranches", "RedundantOperationOnEmptyContainer"}) +public class BaseStreamExample { + Stream returningStreamExample(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.stream(); + } else { + return list.stream(); + } + } + + Stream returningStreamAsParameterExample(Stream s) { + UtMock.assume(s != null); + return s; + } + + @SuppressWarnings("Convert2MethodRef") + boolean filterExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + int prevSize = list.size(); + + int newSize = list.stream().filter(value -> value != null).toArray().length; + + return prevSize != newSize; + } + + Integer[] mapExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + final Function mapper = value -> value * 2; + if (list.contains(null)) { + return list.stream().map(mapper).toArray(Integer[]::new); + } else { + return list.stream().map(mapper).toArray(Integer[]::new); + } + } + + // TODO mapToInt, etc https://github.com/UnitTestBot/UTBotJava/issues/146 + + Object[] flatMapExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + return list.stream().flatMap(value -> Arrays.stream(new Object[]{value, value})).toArray(Object[]::new); + } + + boolean distinctExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + int prevSize = list.size(); + + int newSize = list.stream().distinct().toArray().length; + + return prevSize != newSize; + } + + Integer[] sortedExample(List list) { + UtMock.assume(list != null && list.size() >= 2); + + Integer first = list.get(0); + + int lastIndex = list.size() - 1; + Integer last = list.get(lastIndex); + + UtMock.assume(last < first); + + return list.stream().sorted().toArray(Integer[]::new); + } + + // TODO sorted with custom Comparator + + static int x = 0; + + @SuppressWarnings("ResultOfMethodCallIgnored") + int peekExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + int beforeStaticValue = x; + + final Consumer action = value -> x += value; + if (list.contains(null)) { + list.stream().peek(action); + } else { + list.stream().peek(action); + } + + return beforeStaticValue; + } + + @SuppressWarnings("IfStatementWithIdenticalBranches") + Integer[] limitExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + if (list.size() <= 5) { + return list.stream().limit(5).toArray(Integer[]::new); + } else { + return list.stream().limit(5).toArray(Integer[]::new); + } + } + + @SuppressWarnings("IfStatementWithIdenticalBranches") + Integer[] skipExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + if (list.size() > 5) { + return list.stream().skip(5).toArray(Integer[]::new); + } else { + return list.stream().skip(5).toArray(Integer[]::new); + } + } + + @SuppressWarnings("SimplifyStreamApiCallChains") + int forEachExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + int beforeStaticValue = x; + + final Consumer action = value -> x += value; + if (list.contains(null)) { + list.stream().forEach(action); + } else { + list.stream().forEach(action); + } + + return beforeStaticValue; + } + + @SuppressWarnings("SimplifyStreamApiCallChains") + Object[] toArrayExample(List list) { + UtMock.assume(list != null); + + int size = list.size(); + if (size <= 1) { + return list.stream().toArray(); + } else { + return list.stream().toArray(Integer[]::new); + } + } + + Integer reduceExample(List list) { + UtMock.assume(list != null); + + final int identity = 42; + if (list.isEmpty()) { + return list.stream().reduce(identity, this::nullableSum); + } else { + return list.stream().reduce(identity, this::nullableSum); + } + } + + Optional optionalReduceExample(List list) { + UtMock.assume(list != null); + + int size = list.size(); + + if (size == 0) { + return list.stream().reduce(this::nullableSum); + } + + if (size == 1 && list.get(0) == null) { + return list.stream().reduce(this::nullableSum); + } + + return list.stream().reduce(this::nullableSum); + } + + Double complexReduceExample(List list) { + UtMock.assume(list != null); + + final double identity = 42.0; + final BiFunction accumulator = (Double a, Integer b) -> a + (b != null ? b.doubleValue() : 0.0); + if (list.isEmpty()) { + return list.stream().reduce(identity, accumulator, Double::sum); + } + + // TODO this branch leads to almost infinite analysis +// if (list.contains(null)) { +// return list.stream().reduce(42.0, (Double a, Integer b) -> a + b.doubleValue(), Double::sum); +// } + + return list.stream().reduce( + identity, + accumulator, + Double::sum + ); + } + + Integer collectExample(List list) { + UtMock.assume(list != null); + + if (list.contains(null)) { + return list.stream().collect(IntWrapper::new, IntWrapper::plus, IntWrapper::plus).value; + } else { + return list.stream().collect(IntWrapper::new, IntWrapper::plus, IntWrapper::plus).value; + } + } + + @SuppressWarnings("SimplifyStreamApiCallChains") + Set collectorExample(List list) { + UtMock.assume(list != null); + + return list.stream().collect(Collectors.toSet()); + } + + @SuppressWarnings("RedundantOperationOnEmptyContainer") + Optional minExample(List list) { + UtMock.assume(list != null); + + int size = list.size(); + + if (size == 0) { + return list.stream().min(this::nullableCompareTo); + } + + if (size == 1 && list.get(0) == null) { + return list.stream().min(this::nullableCompareTo); + } + + return list.stream().min(this::nullableCompareTo); + } + + @SuppressWarnings("RedundantOperationOnEmptyContainer") + Optional maxExample(List list) { + UtMock.assume(list != null); + + int size = list.size(); + + if (size == 0) { + return list.stream().max(this::nullableCompareTo); + } + + if (size == 1 && list.get(0) == null) { + return list.stream().max(this::nullableCompareTo); + } + + return list.stream().max(this::nullableCompareTo); + } + + @SuppressWarnings({"ReplaceInefficientStreamCount", "ConstantConditions"}) + long countExample(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.stream().count(); + } else { + return list.stream().count(); + } + } + + @SuppressWarnings({"Convert2MethodRef", "ConstantConditions", "RedundantOperationOnEmptyContainer"}) + boolean anyMatchExample(List list) { + UtMock.assume(list != null); + + final Predicate predicate = value -> value == null; + if (list.isEmpty()) { + return list.stream().anyMatch(predicate); + } + + UtMock.assume(list.size() == 2); + + Integer first = list.get(0); + Integer second = list.get(1); + + if (first == null && second == null) { + return list.stream().anyMatch(predicate); + } + + if (first == null) { + return list.stream().anyMatch(predicate); + } + + if (second == null) { + return list.stream().anyMatch(predicate); + } + + return list.stream().anyMatch(predicate); + } + + @SuppressWarnings({"Convert2MethodRef", "ConstantConditions", "RedundantOperationOnEmptyContainer"}) + boolean allMatchExample(List list) { + UtMock.assume(list != null); + + final Predicate predicate = value -> value == null; + if (list.isEmpty()) { + return list.stream().allMatch(predicate); + } + + UtMock.assume(list.size() == 2); + + Integer first = list.get(0); + Integer second = list.get(1); + + if (first == null && second == null) { + return list.stream().allMatch(predicate); + } + + if (first == null) { + return list.stream().allMatch(predicate); + } + + if (second == null) { + return list.stream().allMatch(predicate); + } + + return list.stream().allMatch(predicate); + } + + @SuppressWarnings({"Convert2MethodRef", "ConstantConditions", "RedundantOperationOnEmptyContainer"}) + boolean noneMatchExample(List list) { + UtMock.assume(list != null); + + final Predicate predicate = value -> value == null; + if (list.isEmpty()) { + return list.stream().noneMatch(predicate); + } + + UtMock.assume(list.size() == 2); + + Integer first = list.get(0); + Integer second = list.get(1); + + if (first == null && second == null) { + return list.stream().noneMatch(predicate); + } + + if (first == null) { + return list.stream().noneMatch(predicate); + } + + if (second == null) { + return list.stream().noneMatch(predicate); + } + + return list.stream().noneMatch(predicate); + } + + @SuppressWarnings("RedundantOperationOnEmptyContainer") + Optional findFirstExample(List list) { + UtMock.assume(list != null); + + if (list.isEmpty()) { + return list.stream().findFirst(); + } + + if (list.get(0) == null) { + return list.stream().findFirst(); + } else { + return list.stream().findFirst(); + } + } + + Integer iteratorSumExample(List list) { + UtMock.assume(list != null && !list.isEmpty()); + + int sum = 0; + Iterator streamIterator = list.stream().iterator(); + + if (list.isEmpty()) { + while (streamIterator.hasNext()) { + Integer value = streamIterator.next(); + sum += value; + } + } else { + while (streamIterator.hasNext()) { + Integer value = streamIterator.next(); + sum += value; + } + } + + return sum; + } + + Stream streamOfExample(Integer[] values) { + UtMock.assume(values != null); + + if (values.length == 0) { + return Stream.empty(); + } else { + return Stream.of(values); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + long closedStreamExample(List values) { + UtMock.assume(values != null); + + Stream stream = values.stream(); + stream.count(); + + return stream.count(); + } + + @SuppressWarnings({"ReplaceInefficientStreamCount", "ConstantConditions"}) + long customCollectionStreamExample(CustomCollection customCollection) { + UtMock.assume(customCollection != null && customCollection.data != null); + + if (customCollection.isEmpty()) { + return customCollection.stream().count(); + + // simplified example, does not generate branch too + /*customCollection.removeIf(Objects::isNull); + return customCollection.toArray().length;*/ + } else { + return customCollection.stream().count(); + + // simplified example, does not generate branch too + /*customCollection.removeIf(Objects::isNull); + return customCollection.toArray().length;*/ + } + } + + @SuppressWarnings({"ConstantConditions", "ReplaceInefficientStreamCount"}) + long anyCollectionStreamExample(Collection c) { + UtMock.assume(c != null); + + if (c.isEmpty()) { + return c.stream().count(); + } else { + return c.stream().count(); + } + } + + Integer[] generateExample() { + return Stream.generate(() -> 42).limit(10).toArray(Integer[]::new); + } + + Integer[] iterateExample() { + return Stream.iterate(42, x -> x + 1).limit(10).toArray(Integer[]::new); + } + + Integer[] concatExample() { + final int identity = 42; + Stream first = Stream.generate(() -> identity).limit(10); + Stream second = Stream.iterate(identity, x -> x + 1).limit(10); + + return Stream.concat(first, second).toArray(Integer[]::new); + } + + // avoid NPE + private int nullableSum(Integer a, Integer b) { + if (b == null) { + return a; + } + + return a + b; + } + + // avoid NPE + private int nullableCompareTo(Integer a, Integer b) { + if (a == null && b == null) { + return 0; + } + + if (a == null) { + return -1; + } + + if (b == null) { + return 1; + } + + return a.compareTo(b); + } + + private static class IntWrapper { + int value = 0; + + void plus(int other) { + value += other; + } + + void plus(IntWrapper other) { + value += other.value; + } + } + + public static class CustomCollection implements Collection { + private E[] data; + + public CustomCollection(@NotNull E[] data) { + this.data = data; + } + + @Override + public int size() { + return data.length; + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public boolean contains(Object o) { + return Arrays.asList(data).contains(o); + } + + @NotNull + @Override + public Iterator iterator() { + return Arrays.asList(data).iterator(); + } + + @SuppressWarnings({"ManualArrayCopy", "unchecked"}) + @NotNull + @Override + public Object[] toArray() { + final int size = size(); + E[] arr = (E[]) new Object[size]; + for (int i = 0; i < size; i++) { + arr[i] = data[i]; + } + + return arr; + } + + @NotNull + @Override + public T[] toArray(@NotNull T[] a) { + return Arrays.asList(data).toArray(a); + } + + @Override + public boolean add(E e) { + final int size = size(); + E[] newData = Arrays.copyOf(data, size + 1); + newData[size] = e; + data = newData; + + return true; + } + + @SuppressWarnings("unchecked") + @Override + public boolean remove(Object o) { + final List es = Arrays.asList(data); + final boolean removed = es.remove(o); + data = (E[]) es.toArray(); + + return removed; + } + + @Override + public boolean containsAll(@NotNull Collection c) { + return Arrays.asList(data).containsAll(c); + } + + @SuppressWarnings("unchecked") + @Override + public boolean addAll(@NotNull Collection c) { + final List es = Arrays.asList(data); + final boolean added = es.addAll(c); + data = (E[]) es.toArray(); + + return added; + } + + @SuppressWarnings("unchecked") + @Override + public boolean removeAll(@NotNull Collection c) { + final List es = Arrays.asList(data); + final boolean removed = es.removeAll(c); + data = (E[]) es.toArray(); + + return removed; + } + + @SuppressWarnings("unchecked") + @Override + public boolean retainAll(@NotNull Collection c) { + final List es = Arrays.asList(data); + final boolean retained = es.retainAll(c); + data = (E[]) es.toArray(); + + return retained; + } + + @SuppressWarnings("unchecked") + @Override + public void clear() { + data = (E[]) new Object[0]; + } + } +} diff --git a/utbot-sample/src/main/java/org/utbot/examples/strings/StringExamples.java b/utbot-sample/src/main/java/org/utbot/examples/strings/StringExamples.java index 3b746feb74..73c624ae92 100644 --- a/utbot-sample/src/main/java/org/utbot/examples/strings/StringExamples.java +++ b/utbot-sample/src/main/java/org/utbot/examples/strings/StringExamples.java @@ -1,5 +1,7 @@ package org.utbot.examples.strings; +import java.util.Arrays; + import static java.lang.Boolean.valueOf; class IntPair { @@ -411,4 +413,8 @@ public String equalsIgnoreCase(String s) { return "failure"; } } + + public String listToString() { + return Arrays.asList("a", "b", "c").toString(); + } } diff --git a/utbot-summary-tests/build.gradle b/utbot-summary-tests/build.gradle new file mode 100644 index 0000000000..cb460cbd1d --- /dev/null +++ b/utbot-summary-tests/build.gradle @@ -0,0 +1,17 @@ +apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" +apply plugin: "java" + +evaluationDependsOn(':utbot-framework') +compileTestJava.dependsOn tasks.getByPath(':utbot-framework:testClasses') + +dependencies { + implementation(project(":utbot-framework")) + implementation(project(':utbot-instrumentation')) + testImplementation project(':utbot-sample') + testImplementation group: 'junit', name: 'junit', version: junit4_version + testCompile project(':utbot-framework').sourceSets.test.output +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/SummaryTestCaseGeneratorTest.kt b/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt similarity index 68% rename from utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/SummaryTestCaseGeneratorTest.kt rename to utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt index 1cdca4b148..5ba1fb5dab 100644 --- a/utbot-analytics/src/test/kotlin/org/utbot/analytics/examples/SummaryTestCaseGeneratorTest.kt +++ b/utbot-summary-tests/src/test/kotlin/examples/SummaryTestCaseGeneratorTest.kt @@ -1,11 +1,11 @@ -package org.utbot.analytics.examples +package examples +import org.junit.jupiter.api.* import org.utbot.common.WorkaroundReason import org.utbot.common.workaround import org.utbot.examples.AbstractTestCaseGeneratorTest import org.utbot.examples.CoverageMatcher import org.utbot.examples.DoNotCalculate -import org.utbot.framework.UtSettings.checkNpeForFinalFields import org.utbot.framework.UtSettings.checkNpeInNestedMethods import org.utbot.framework.UtSettings.checkNpeInNestedNotPrivateMethods import org.utbot.framework.UtSettings.checkSolverTimeoutMillis @@ -15,6 +15,7 @@ import org.utbot.framework.plugin.api.MockStrategyApi import org.utbot.framework.plugin.api.UtExecution import org.utbot.framework.plugin.api.UtMethod import org.utbot.framework.plugin.api.util.UtContext +import org.utbot.summary.comment.nextSynonyms import org.utbot.summary.summarize import kotlin.reflect.KClass import kotlin.reflect.KFunction @@ -22,15 +23,12 @@ import kotlin.reflect.KFunction1 import kotlin.reflect.KFunction2 import kotlin.reflect.KFunction3 import kotlin.reflect.KFunction4 -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Tag -@Tag("Summary") + +@Disabled open class SummaryTestCaseGeneratorTest( testClass: KClass<*>, - testCodeGeneration: Boolean = true, + testCodeGeneration: Boolean = false, languagePipelines: List = listOf( CodeGenerationLanguageLastStage(CodegenLanguage.JAVA), CodeGenerationLanguageLastStage(CodegenLanguage.KOTLIN, TestExecution) @@ -53,38 +51,43 @@ open class SummaryTestCaseGeneratorTest( coverage: CoverageMatcher = DoNotCalculate, mockStrategy: MockStrategyApi = MockStrategyApi.NO_MOCKS, summaryKeys: List, + methodNames: List = listOf(), displayNames: List = listOf() - ) = check(method, mockStrategy, coverage, summaryKeys, displayNames) + ) = check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) protected inline fun checkOneArgument( method: KFunction2<*, T, R>, coverage: CoverageMatcher = DoNotCalculate, mockStrategy: MockStrategyApi = MockStrategyApi.NO_MOCKS, summaryKeys: List, + methodNames: List = listOf(), displayNames: List = listOf() - ) = check(method, mockStrategy, coverage, summaryKeys, displayNames) + ) = check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) protected inline fun checkTwoArguments( method: KFunction3<*, T1, T2, R>, coverage: CoverageMatcher = DoNotCalculate, mockStrategy: MockStrategyApi = MockStrategyApi.NO_MOCKS, summaryKeys: List, + methodNames: List = listOf(), displayNames: List = listOf() - ) = check(method, mockStrategy, coverage, summaryKeys, displayNames) + ) = check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) protected inline fun checkThreeArguments( method: KFunction4<*, T1, T2, T3, R>, coverage: CoverageMatcher = DoNotCalculate, mockStrategy: MockStrategyApi = MockStrategyApi.NO_MOCKS, summaryKeys: List, + methodNames: List = listOf(), displayNames: List = listOf() - ) = check(method, mockStrategy, coverage, summaryKeys, displayNames) + ) = check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) inline fun check( method: KFunction, mockStrategy: MockStrategyApi, coverageMatcher: CoverageMatcher, summaryKeys: List, + methodNames: List, displayNames: List ) { workaround(WorkaroundReason.HACK) { @@ -92,15 +95,32 @@ open class SummaryTestCaseGeneratorTest( checkSolverTimeoutMillis = 0 checkNpeInNestedMethods = true checkNpeInNestedNotPrivateMethods = true - checkNpeForFinalFields = true } val utMethod = UtMethod.from(method) val testCase = executionsModel(utMethod, mockStrategy) testCase.summarize(searchDirectory) + testCase.executions.checkMatchersWithTextSummary(summaryKeys) + testCase.executions.checkMatchersWithMethodNames(methodNames) testCase.executions.checkMatchersWithDisplayNames(displayNames) } + /** + * It removes from the String all whitespaces, tabs etc. + * + * Also, it replaces all randomly added words from [nextSynonyms] that totally influence on the determinism in test name generation. + * + * @see Explanation of the used regular expression. + */ + private fun String.normalize(): String { + var result = this.replace("\\s+".toRegex(), "") + nextSynonyms.forEach { + result = result.replace(it, "") + } + return result + } + + fun List.checkMatchersWithTextSummary( summaryTextKeys: List, ) { @@ -108,9 +128,22 @@ open class SummaryTestCaseGeneratorTest( return } val notMatchedExecutions = this.filter { execution -> - summaryTextKeys.none { summaryKey -> execution.summary?.contains(summaryKey) == true } + summaryTextKeys.none { summaryKey -> val normalize = execution.summary?.toString()?.normalize() + normalize?.contains(summaryKey.normalize()) == true } } - Assertions.assertTrue(notMatchedExecutions.isEmpty()) { "Not matched summaries ${summaries(notMatchedExecutions)}" } + Assertions.assertTrue(notMatchedExecutions.isEmpty()) { "Not matched comments ${summaries(notMatchedExecutions)}" } + } + + fun List.checkMatchersWithMethodNames( + methodNames: List, + ) { + if (methodNames.isEmpty()) { + return + } + val notMatchedExecutions = this.filter { execution -> + methodNames.none { methodName -> execution.testMethodName?.equals(methodName) == true } + } + Assertions.assertTrue(notMatchedExecutions.isEmpty()) { "Not matched test names ${summaries(notMatchedExecutions)}" } } fun List.checkMatchersWithDisplayNames( @@ -122,7 +155,7 @@ open class SummaryTestCaseGeneratorTest( val notMatchedExecutions = this.filter { execution -> displayNames.none { displayName -> execution.displayName?.equals(displayName) == true } } - Assertions.assertTrue(notMatchedExecutions.isEmpty()) { "Not matched summaries ${summaries(notMatchedExecutions)}" } + Assertions.assertTrue(notMatchedExecutions.isEmpty()) { "Not matched display names ${summaries(notMatchedExecutions)}" } } private fun summaries(executions: List): String { @@ -132,5 +165,4 @@ open class SummaryTestCaseGeneratorTest( } return result } - } \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryReturnExampleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryReturnExampleTest.kt new file mode 100644 index 0000000000..a12c2854e6 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/algorithms/SummaryReturnExampleTest.kt @@ -0,0 +1,506 @@ +package examples.algorithms + +import examples.SummaryTestCaseGeneratorTest +import org.utbot.examples.algorithms.ReturnExample +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryReturnExampleTest : SummaryTestCaseGeneratorTest( + ReturnExample::class, +) { + @Test + fun testCompare() { + val summary1 = "Test executes conditions:\n" + + " (a < 0): True\n" + + "returns from:\n" + + " 1st return statement: return a;\n" + val summary2 = "Test executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): True\n" + + "returns from:\n" + + " 1st return statement: return a;\n" + val summary3 = "Test executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): True\n" + + "returns from:\n" + + " 1st return statement: return c;\n" + val summary4 = "Test executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): True\n" + + "returns from:\n" + + " 2nd return statement: return a;\n" + val summary5 = "Test executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): False\n" + + "returns from:\n" + + " 2nd return statement: return c;\n" + val summary6 = "Test executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): True\n" + + "returns from: return b;\n" + + val methodName1 = "testCompare_ALessThanZero" + val methodName2 = "testCompare_BLessThanZero" + val methodName3 = "testCompare_BEquals10" + val methodName4 = "testCompare_ALessThanB" + val methodName5 = "testCompare_AGreaterOrEqualB" + val methodName6 = "testCompare_AGreaterThanB" + + val displayName1 = "a < 0 : False -> return a" + val displayName2 = "b < 0 : True -> return a" + val displayName3 = "b == 10 : True -> return c" + val displayName4 = "a < b : True -> return a" + val displayName5 = "a < b : False -> return c" + val displayName6 = "a > b : True -> return b" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6 + ) + + val method = ReturnExample::compare + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCompareChar() { + val summary1 = "Test executes conditions:\n" + + " (n < 1): True\n" + + "returns from: return ' ';\n" + val summary2 = "Test executes conditions:\n" + + " (n < 1): False\n" + + "iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): True\n" + + "returns from: return b;" + val summary3 = "Test executes conditions:\n" + + " (n < 1): False\n" + + "iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): True\n" + + "returns from:\n" + + " 1st return statement: return a;" + val summary4 = "Test executes conditions:\n" + + " (n < 1): False\n" + + "iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): False\n" + + "Test then returns from:\n" + + " 2nd return statement: return a;\n" + + val methodName1 = "testCompareChars_NLessThan1" + val methodName2 = "testCompareChars_0OfCharactertoCharsiEqualsA" // TODO: a weird unclear naming + val methodName3 = "testCompareChars_0OfCharactertoCharsiEqualsB" + val methodName4 = "testCompareChars_0OfCharactertoCharsiNotEqualsB" // TODO: si -> is + + val displayName1 = "n < 1 : True -> return ' '" + val displayName2 = "Character.toChars(i)[0] == a : True -> return b" + val displayName3 = "Character.toChars(i)[0] == b : True -> return a" + val displayName4 = "Character.toChars(i)[0] == b : False -> return a" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + val method = ReturnExample::compareChars + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testInnerVoidCompareChars() { + val summary1 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): True\n" + + " returns from: return ' ';\n" + + " " // TODO: generates empty String or \n a the end + val summary2 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): True\n" + + " returns from: return b;" + val summary3 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): False\n" + + " Test then returns from: return a;\n" + + " " // TODO: generates empty String or \n a the end + val summary4 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): True\n" + + " returns from: return a;" + + val methodName1 = "testInnerVoidCompareChars_NLessThan1" + val methodName2 = "testInnerVoidCompareChars_0OfCharactertoCharsiEqualsA" // TODO: a weird unclear naming + val methodName3 = "testInnerVoidCompareChars_0OfCharactertoCharsiNotEqualsB" + val methodName4 = "testInnerVoidCompareChars_0OfCharactertoCharsiEqualsB" // TODO: si -> is + + val displayName1 = "n < 1 : True -> return ' '" + val displayName2 = "Character.toChars(i)[0] == a : True -> return b" + val displayName3 = "Character.toChars(i)[0] == b : False -> return a" + val displayName4 = "Character.toChars(i)[0] == b : True -> return a" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + val method = ReturnExample::innerVoidCompareChars + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testInnerReturnCompareChars() { + val summary1 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): True\n" + + " returns from: return ' ';\n" + + " \n" + + "Test later returns from: return compareChars(a, b, n);\n" + val summary2 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): True\n" + + " returns from: return b;\n" + + "Test later returns from: return compareChars(a, b, n);\n" + val summary3 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): False\n" + + " Test then returns from: return a;\n" + + " \n" + // + "Test afterwards returns from: return compareChars(a, b, n);\n" + val summary4 = "Test calls ReturnExample::compareChars,\n" + + " there it executes conditions:\n" + + " (n < 1): False\n" + + " iterates the loop for(int i = 0; i < n; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (Character.toChars(i)[0] == a): False,\n" + + " (Character.toChars(i)[0] == b): True\n" + + " returns from: return a;\n" + + "Test afterwards returns from: return compareChars(a, b, n);\n" + + val methodName1 = "testInnerReturnCompareChars_NLessThan1" + val methodName2 = "testInnerReturnCompareChars_0OfCharactertoCharsiEqualsA" // TODO: a weird unclear naming + val methodName3 = "testInnerReturnCompareChars_0OfCharactertoCharsiNotEqualsB" + val methodName4 = "testInnerReturnCompareChars_0OfCharactertoCharsiEqualsB" // TODO: si -> is + + val displayName1 = "n < 1 : True -> return ' '" + val displayName2 = "Character.toChars(i)[0] == a : True -> return b" + val displayName3 = "Character.toChars(i)[0] == b : False -> return a" + val displayName4 = "Character.toChars(i)[0] == b : True -> return a" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + val method = ReturnExample::innerReturnCompareChars + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testInnerVoidCompare() { + val summary1 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): True\n" + + " returns from: return a;\n" + + " " // TODO: remove blank line + val summary2 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): True\n" + + " returns from: return b;\n" + + " " // TODO: remove blank line + val summary3 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): True\n" + + " returns from: return c;\n" + + " " // TODO: remove blank line + val summary4 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): True\n" + + " returns from: return a;\n" + + " " // TODO: remove blank line + val summary5 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): True\n" + + " returns from: return a;\n" + + " " // TODO: remove blank line + val summary6 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): False\n" + + " returns from: return c;\n" + + " " // TODO: remove blank line + + val methodName1 = "testInnerVoidCallCompare_BLessThanZero" + val methodName2 = "testInnerVoidCallCompare_AGreaterThanB" + val methodName3 = "testInnerVoidCallCompare_BEquals10" + val methodName4 = "testInnerVoidCallCompare_ALessThanZero" + val methodName5 = "testInnerVoidCallCompare_ALessThanB" + val methodName6 = "testInnerVoidCallCompare_AGreaterOrEqualB" + + val displayName1 = "b < 0 : True -> return a" + val displayName2 = "a > b : True -> return b" + val displayName3 = "b == 10 : True -> return c" + val displayName4 = "a < 0 : False -> return a" + val displayName5 = "a < b : True -> return a" + val displayName6 = "a < b : False -> return c" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6 + ) + + val method = ReturnExample::innerVoidCallCompare + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testInnerReturnCompare() { + val summary1 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): True\n" + + " returns from: return a;\n" + + " \n" + + "Test then returns from: return compare(a, b);\n" + val summary2 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): True\n" + + " returns from: return b;\n" + + " \n" + + "Test afterwards returns from: return compare(a, b);\n" + val summary3 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): True\n" + + " returns from: return c;\n" + + " \n" + + "Test then returns from: return compare(a, b);\n" + val summary4 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): True\n" + + " returns from: return a;\n" + + " \n" + + "Test next returns from: return compare(a, b);\n" + val summary5 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): True\n" + + " returns from: return a;\n" + + " \n" + + "Test afterwards returns from: return compare(a, b);\n" + val summary6 = "Test calls ReturnExample::compare,\n" + + " there it executes conditions:\n" + + " (a < 0): False,\n" + + " (b < 0): False,\n" + + " (b == 10): False,\n" + + " (a > b): False,\n" + + " (a < b): False\n" + + " returns from: return c;\n" + + " \n" + + "Test next returns from: return compare(a, b);\n" + + val methodName1 = "testInnerReturnCallCompare_BLessThanZero" + val methodName2 = "testInnerReturnCallCompare_AGreaterThanB" + val methodName3 = "testInnerReturnCallCompare_BEquals10" + val methodName4 = "testInnerReturnCallCompare_ALessThanZero" + val methodName5 = "testInnerReturnCallCompare_ALessThanB" + val methodName6 = "testInnerReturnCallCompare_AGreaterOrEqualB" + + val displayName1 = + "b < 0 : True -> return a" // TODO: the same display names for many tests with different test names + val displayName2 = "a > b : True -> return b" + val displayName3 = "b == 10 : True -> return c" + val displayName4 = "a < 0 : False -> return a" + val displayName5 = "a < b : True -> return a" + val displayName6 = "a < b : False -> return c" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6 + ) + + val method = ReturnExample::innerReturnCallCompare + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt new file mode 100644 index 0000000000..b4e3a6f3ba --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/controlflow/SummaryCycleTest.kt @@ -0,0 +1,133 @@ +package examples.controlflow + +import examples.SummaryTestCaseGeneratorTest +import org.junit.Ignore +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.utbot.examples.controlflow.Cycles +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.algorithms.ReturnExample +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryCycleTest : SummaryTestCaseGeneratorTest( + Cycles::class, +) { + @Test + fun testLoopInsideLoop() { + val summary1 = "Test iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): True\n" + + "returns from: return 2;" + val summary2 = "Test does not iterate for(int i = x - 5; i < x; i++), for(int j = i; j < x + i; j++), returns from: return -1;\n" // TODO: should it be formatted from the new string? + val summary3 = "Test iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + "iterates the loop for(int j = i; j < x + i; j++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): True\n" + + "returns from: return 1;" + val summary4 = "Test iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + "iterates the loop for(int j = i; j < x + i; j++) twice,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): False\n" + + " (j == 7): True\n" + + "returns from: return 1;" + val summary5 = "Test iterates the loop for(int i = x - 5; i < x; i++) 5 times. \n" + + "Test afterwards does not iterate for(int j = i; j < x + i; j++), returns from: return -1;\n" // TODO: should it be formatted with separation of code? + + val methodName1 = "testLoopInsideLoop_ILessThanZero" + val methodName2 = "testLoopInsideLoop_ReturnNegative1" + val methodName3 = "testLoopInsideLoop_JEquals7" + val methodName4 = "testLoopInsideLoop_JNotEquals7" + val methodName5 = "testLoopInsideLoop_ReturnNegative1_1" + + + val displayName1 = "i < 0 : True -> return 2" + val displayName2 = "-> return -1" // TODO: add something before -> + val displayName3 = "i < 0 : False -> return 1" + val displayName4 = "j == 7 : False -> return 1" + val displayName5 = "-> return -1" + + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5 + ) + + val method = Cycles::loopInsideLoop + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testStructureLoop() { + val summary1 = "Test does not iterate for(int i = 0; i < x; i++), returns from: return -1;\n" + val summary2 = "Test iterates the loop for(int i = 0; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i == 2): False\n" + + "Test further returns from: return -1;\n" + val summary3 = "Test iterates the loop for(int i = 0; i < x; i++) 3 times,\n" + + " inside this loop, the test executes conditions:\n" + + " (i == 2): True\n" + + "returns from: return 1;" + + val methodName1 = "testStructureLoop_ReturnNegative1" + val methodName2 = "testStructureLoop_INotEquals2" + val methodName3 = "testStructureLoop_IEquals2" + + + val displayName1 = "-> return -1" + val displayName2 = "i == 2 : False -> return -1" + val displayName3 = "i == 2 : True -> return 1" + + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + val method = Cycles::structureLoop + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryInnerCallsTest.kt b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryInnerCallsTest.kt new file mode 100644 index 0000000000..005ff1d64c --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryInnerCallsTest.kt @@ -0,0 +1,825 @@ +package examples.inner + +import examples.SummaryTestCaseGeneratorTest +import guava.examples.math.Stats +import org.junit.Ignore +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.utbot.examples.inner.InnerCalls +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.controlflow.Cycles +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryInnerCallsTest : SummaryTestCaseGeneratorTest( + InnerCalls::class, +) { + @Test + fun testCallLoopInsideLoop() { + val summary1 = "Test calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + " iterates the loop for(int j = i; j < x + i; j++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): True\n" + + " returns from: return 1;\n" + + "Test afterwards returns from: return cycles.loopInsideLoop(x);\n" + val summary2 = "Test calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): True\n" + + " returns from: return 2;\n" + + "Test then returns from: return cycles.loopInsideLoop(x);\n" + val summary3 = "Test calls Cycles::loopInsideLoop,\n" + + " there it does not iterate for(int i = x - 5; i < x; i++), for(int j = i; j < x + i; j++), returns from: return -1;\n" + + " \n" + + "Test later returns from: return cycles.loopInsideLoop(x);\n" + val summary4 = "Test calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + " iterates the loop for(int j = i; j < x + i; j++) twice,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): False\n" + + " (j == 7): True\n" + + " returns from: return 1;\n" + + "Test later returns from: return cycles.loopInsideLoop(x);\n" + val summary5 = "Test calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) 5 times. \n" + + " Test further does not iterate for(int j = i; j < x + i; j++), returns from: return -1;\n" + + " \n" + + "Test then returns from: return cycles.loopInsideLoop(x);\n" + + val methodName1 = "testCallLoopInsideLoop_JEquals7" + val methodName2 = "testCallLoopInsideLoop_ILessThanZero" + val methodName3 = "testCallLoopInsideLoop_ReturnNegative1" + val methodName4 = "testCallLoopInsideLoop_JNotEquals7" + val methodName5 = "testCallLoopInsideLoop_ReturnNegative1_1" + + + val displayName1 = "i < 0 : False -> return 1" + val displayName2 = "i < 0 : True -> return 2" + val displayName3 = "loopInsideLoop -> return -1" + val displayName4 = "j == 7 : False -> return 1" + val displayName5 = "loopInsideLoop -> return -1" + + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5 + ) + + val method = InnerCalls::callLoopInsideLoop + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallLeftBinSearch() { + //NOTE: 5 and 6 cases has different paths but throws the equal exception. + val summary1 = "Test calls BinarySearch::leftBinSearch,\n" + + " there it does not iterate while(left < right - 1), executes conditions:\n" + + " (found): False\n" + + " returns from: return -1;\n" + + " \n" + + "Test then returns from: return binarySearch.leftBinSearch(array, key);\n" + val summary2 = "Test calls BinarySearch::leftBinSearch,\n" + + " there it iterates the loop while(left < right - 1) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (array[middle] == key): False,\n" + + " (array[middle] < key): True\n" + + " Test afterwards executes conditions:\n" + + " (found): False\n" + + " returns from: return -1;\n" + + " \n" + + "Test next returns from: return binarySearch.leftBinSearch(array, key);\n" + val summary3 = "Test calls BinarySearch::leftBinSearch,\n" + + " there it iterates the loop while(left < right - 1) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (array[middle] == key): False,\n" + + " (array[middle] < key): False\n" + + " Test afterwards executes conditions:\n" + + " (found): False\n" + + " returns from: return -1;\n" + + " \n" + + "Test next returns from: return binarySearch.leftBinSearch(array, key);\n" + val summary4 = "Test calls BinarySearch::leftBinSearch,\n" + + " there it iterates the loop while(left < right - 1) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (array[middle] == key): True,\n" + + " (array[middle] < key): False\n" + + " Test then executes conditions:\n" + + " (found): True\n" + + " returns from: return right + 1;\n" + + " \n" + + "Test further returns from: return binarySearch.leftBinSearch(array, key);\n" + val summary5 = "Test \n" + + "throws IllegalArgumentException in: return binarySearch.leftBinSearch(array, key);\n" + val summary6 = "Test \n" + + "throws IllegalArgumentException in: return binarySearch.leftBinSearch(array, key);\n" + val summary7 = "Test calls BinarySearch::leftBinSearch,\n" + + " there it invokes:\n" + + " BinarySearch::isUnsorted once\n" + + " triggers recursion of leftBinSearch once, \n" + + "Test throws NullPointerException in: return binarySearch.leftBinSearch(array, key);\n" + + val methodName1 = "testCallLeftBinSearch_NotFound" + val methodName2 = "testCallLeftBinSearch_MiddleOfArrayLessThanKey" + val methodName3 = "testCallLeftBinSearch_NotFound_1" + val methodName4 = "testCallLeftBinSearch_Found" + val methodName5 = "testCallLeftBinSearch_ThrowIllegalArgumentException" + val methodName6 = "testCallLeftBinSearch_ThrowIllegalArgumentException_1" + val methodName7 = "testCallLeftBinSearch_BinarySearchIsUnsorted" + + + val displayName1 = "found : False -> return -1" + val displayName2 = "array[middle] < key : True -> return -1" + val displayName3 = "while(left < right - 1) -> return -1" + val displayName4 = "array[middle] == key : True -> return right + 1" + val displayName5 = "return binarySearch.leftBinSearch(array, key) : True -> ThrowIllegalArgumentException" // TODO: probably return statement could be removed + val displayName6 = "return binarySearch.leftBinSearch(array, key) : True -> ThrowIllegalArgumentException" + val displayName7 = "return binarySearch.leftBinSearch(array, key) : True -> ThrowNullPointerException" + + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6, + summary7 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6, + displayName7 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7 + ) + + val method = InnerCalls::callLeftBinSearch + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + // TODO: SAT-1211 + @Test + fun testCallCreateNewThreeDimensionalArray() { + val summary1 = "Test calls ArrayOfArrays::createNewThreeDimensionalArray,\n" + + " there it executes conditions:\n" + + " (length != 2): True\n" + + " returns from: return new int[0][][];\n" + + " " + val summary2 = "Test calls ArrayOfArrays::createNewThreeDimensionalArray,\n" + + " there it executes conditions:\n" + + " (length != 2): False\n" + + " iterates the loop for(int i = 0; i < length; i++) once,\n" + + " inside this loop, the test iterates the loop for(int j = 0; j < length; j++) once,\n" + + " inside this loop, the test iterates the loop for(int k = 0; k < length; k++)\n" + + " Test then returns from: return matrix;\n" + + " " + + val methodName1 = "testCallCreateNewThreeDimensionalArray_LengthNotEquals2" + val methodName2 = "testCallCreateNewThreeDimensionalArray_LengthEquals2" + + val displayName1 = "length != 2 : True -> return new int[0][][]" + val displayName2 = "length != 2 : False -> return matrix" + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + val method = InnerCalls::callCreateNewThreeDimensionalArray + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallInitExamples() { + // NOTE: paths are different for test cases 1 and 2 + val summary1 = "Test calls ExceptionExamples::initAnArray,\n" + + " there it catches exception:\n" + + " IndexOutOfBoundsException e\n" + + " returns from: return -3;\n" + + " \n" + + "Test later returns from: return exceptionExamples.initAnArray(n);\n" + val summary2 = "Test calls ExceptionExamples::initAnArray,\n" + + " there it catches exception:\n" + + " IndexOutOfBoundsException e\n" + + " returns from: return -3;\n" + + " \n" + + "Test then returns from: return exceptionExamples.initAnArray(n);\n" + val summary3 = "Test calls ExceptionExamples::initAnArray,\n" + + " there it catches exception:\n" + + " NegativeArraySizeException e\n" + + " returns from: return -2;\n" + + " \n" + + "Test next returns from: return exceptionExamples.initAnArray(n);\n" + val summary4 = "Test calls ExceptionExamples::initAnArray,\n" + + " there it returns from: return a[n - 1] + a[n - 2];\n" + + " \n" + + "Test afterwards returns from: return exceptionExamples.initAnArray(n);\n" + + val methodName1 = "testCallInitExamples_CatchIndexOutOfBoundsException" + val methodName2 = "testCallInitExamples_CatchIndexOutOfBoundsException_1" + val methodName3 = "testCallInitExamples_CatchNegativeArraySizeException" + val methodName4 = "testCallInitExamples_ReturnN1OfAPlusN2OfA" + + val displayName1 = "Catch (IndexOutOfBoundsException e) -> return -3" + val displayName2 = "Catch (IndexOutOfBoundsException e) -> return -3" + val displayName3 = "Catch (NegativeArraySizeException e) -> return -2" + val displayName4 = "initAnArray -> return a[n - 1] + a[n - 2]" + + val method = InnerCalls::callInitExamples + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallFactorial() { + val summary1 = "Test calls Recursion::factorial,\n" + + " there it executes conditions:\n" + + " (n == 0): True\n" + + " returns from: return 1;\n" + + " \n" + + "Test next returns from: return r.factorial(n);\n" + val summary2 = "Test calls Recursion::factorial,\n" + + " there it executes conditions:\n" + + " (n == 0): False\n" + + " triggers recursion of factorial once, returns from: return n * factorial(n - 1);\n" + + " \n" + + "Test further returns from: return r.factorial(n);\n" + val summary3 = "Test calls Recursion::factorial,\n" + + " there it executes conditions:\n" + + " (n < 0): True\n" + + " triggers recursion of factorial once, \n" + + "Test throws IllegalArgumentException in: return r.factorial(n);\n" + + val methodName1 = "testCallFactorial_NEqualsZero" + val methodName2 = "testCallFactorial_NNotEqualsZero" + val methodName3 = "testCallFactorial_NLessThanZero" + + val displayName1 = "n == 0 : True -> return 1" + val displayName2 = "n == 0 : False -> return n * factorial(n - 1)" + val displayName3 = "return r.factorial(n) : True -> ThrowIllegalArgumentException" + + val method = InnerCalls::callFactorial + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallSimpleInvoke() { + val summary1 = "Test calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): False,\n" + + " (snd < 100): True\n" + + " \n" + + "Test throws IllegalArgumentException in: return invokeExample.simpleFormula(f, s);\n" + val summary2 = "Test calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): True\n" + + " \n" + + "Test throws IllegalArgumentException in: return invokeExample.simpleFormula(f, s);\n" + val summary3 = "Test calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): False,\n" + + " (snd < 100): False\n" + + " invokes:\n" + + " InvokeExample::half once,\n" + + " InvokeExample::mult once\n" + + " returns from: return mult(x, y);\n" + + " \n" + + "Test then returns from: return invokeExample.simpleFormula(f, s);\n" + + val methodName1 = "testCallSimpleInvoke_SndLessThan100" + val methodName2 = "testCallSimpleInvoke_FstLessThan100" + val methodName3 = "testCallSimpleInvoke_SndGreaterOrEqual100" + + val displayName1 = "return invokeExample.simpleFormula(f, s) : True -> ThrowIllegalArgumentException" + val displayName2 = "return invokeExample.simpleFormula(f, s) : True -> ThrowIllegalArgumentException" + val displayName3 = "fst < 100 : True -> return mult(x, y)" + + val method = InnerCalls::callSimpleInvoke + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallComplicatedMethod() { + val summary1 = "Test calls StringExamples::indexOf,\n" + + " there it invokes:\n" + + " String::indexOf once\n" + + " triggers recursion of indexOf once, \n" + + "Test throws NullPointerException in: return stringExamples.indexOf(s, key);\n" + val summary2 = "Test calls StringExamples::indexOf,\n" + + " there it invokes:\n" + + " String::indexOf once\n" + + " \n" + + "Test throws NullPointerException \n" + val summary3 = "Test calls StringExamples::indexOf,\n" + + " there it executes conditions:\n" + + " (i > 0): False,\n" + + " (i == 0): True\n" + + " returns from: return i;\n" + + " \n" + + "Test further returns from: return stringExamples.indexOf(s, key);\n" + val summary4 = "Test calls StringExamples::indexOf,\n" + + " there it executes conditions:\n" + + " (i > 0): False,\n" + + " (i == 0): False\n" + + " returns from: return i;\n" + + " \n" + + "Test later returns from: return stringExamples.indexOf(s, key);\n" + val summary5 = "Test calls StringExamples::indexOf,\n" + + " there it executes conditions:\n" + + " (i > 0): True\n" + + " returns from: return i;\n" + + " \n" + + "Test afterwards returns from: return stringExamples.indexOf(s, key);\n" + + val methodName1 = "testCallStringExample_StringIndexOf" + val methodName2 = "testCallStringExample_StringIndexOf_1" + val methodName3 = "testCallStringExample_IEqualsZero" + val methodName4 = "testCallStringExample_INotEqualsZero" + val methodName5 = "testCallStringExample_IGreaterThanZero" + + val displayName1 = "return stringExamples.indexOf(s, key) : True -> ThrowNullPointerException" + val displayName2 = " -> ThrowNullPointerException" + val displayName3 = "i == 0 : True -> return i" + val displayName4 = "i == 0 : False -> return i" + val displayName5 = "i > 0 : True -> return i" + + val method = InnerCalls::callStringExample + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallSimpleSwitch() { + val summary1 = "Test calls Switch::simpleSwitch,\n" + + " there it activates switch case: 12, returns from: return 12;\n" + + " " + val summary2 = "Test calls Switch::simpleSwitch,\n" + + " there it activates switch case: 13, returns from: return 13;\n" + + " " + val summary3 = "Test calls Switch::simpleSwitch,\n" + + " there it activates switch case: 10, returns from: return 10;\n" + + " " + val summary4 = "Test calls Switch::simpleSwitch,\n" + + " there it activates switch case: default, returns from: return -1;\n" + + " " + + val methodName1 = "testCallSimpleSwitch_Return12" + val methodName2 = "testCallSimpleSwitch_Return13" + val methodName3 = "testCallSimpleSwitch_Return10" + val methodName4 = "testCallSimpleSwitch_ReturnNegative1" + + val displayName1 = "switch(x) case: 12 -> return 12" + val displayName2 = "switch(x) case: 13 -> return 13" + val displayName3 = "switch(x) case: 10 -> return 10" + val displayName4 = "switch(x) case: Default -> return -1" + + val method = InnerCalls::callSimpleSwitch + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testCallLookup() { + val summary1 = "Test calls Switch::lookupSwitch,\n" + + " there it activates switch case: 20, returns from: return 20;\n" + + " " + val summary2 = "Test calls Switch::lookupSwitch,\n" + + " there it activates switch case: 30, returns from: return 30;\n" + + " " + val summary3 = "Test calls Switch::lookupSwitch,\n" + + " there it activates switch case: 0, returns from: return 0;\n" + + " " + val summary4 = "Test calls Switch::lookupSwitch,\n" + + " there it activates switch case: default, returns from: return -1;\n" + + " " + + val methodName1 = "testCallLookup_Return20" + val methodName2 = "testCallLookup_Return30" + val methodName3 = "testCallLookup_ReturnZero" + val methodName4 = "testCallLookup_ReturnNegative1" + + val displayName1 = "switch(x) case: 20 -> return 20" + val displayName2 = "switch(x) case: 30 -> return 30" + val displayName3 = "switch(x) case: 0 -> return 0" + val displayName4 = "switch(x) case: Default -> return -1" + + val method = InnerCalls::callLookup + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testDoubleCall() { + val summary1 = "Test calls InnerCalls::callSimpleInvoke,\n" + + " there it calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): True\n" + + " \n" + + "Test throws IllegalArgumentException in: callSimpleInvoke(f, s);\n" + val summary2 = "Test calls InnerCalls::callSimpleInvoke,\n" + + " there it calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): False,\n" + + " (snd < 100): True\n" + + " \n" + + "Test throws IllegalArgumentException in: callSimpleInvoke(f, s);\n" + val summary3 = "Test calls InnerCalls::callSimpleInvoke,\n" + + " there it calls InvokeExample::simpleFormula,\n" + + " there it executes conditions:\n" + + " (fst < 100): False,\n" + + " (snd < 100): False\n" + + " invokes:\n" + + " InvokeExample::half once,\n" + + " InvokeExample::mult once\n" + + " returns from: return mult(x, y);\n" + + " \n" + + " Test later returns from: return invokeExample.simpleFormula(f, s);\n" + + " " + + val methodName1 = "testDoubleSimpleInvoke_FstLessThan100" + val methodName2 = "testDoubleSimpleInvoke_SndLessThan100" + val methodName3 = "testDoubleSimpleInvoke_SndGreaterOrEqual100" + + val displayName1 = "callSimpleInvoke(f, s) : True -> ThrowIllegalArgumentException" + val displayName2 = "callSimpleInvoke(f, s) : True -> ThrowIllegalArgumentException" + val displayName3 = "fst < 100 : True -> return mult(x, y)" + + val method = InnerCalls::doubleSimpleInvoke + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testDoubleCallLoopInsideLoop() { + val summary1 = "Test calls InnerCalls::callLoopInsideLoop,\n" + + " there it calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): True\n" + + " returns from: return 2;\n" + + " Test afterwards returns from: return cycles.loopInsideLoop(x);\n" + + " \n" + + "Test further returns from: return result;\n" + val summary2 = "Test calls InnerCalls::callLoopInsideLoop,\n" + + " there it calls Cycles::loopInsideLoop,\n" + + " there it does not iterate for(int i = x - 5; i < x; i++), for(int j = i; j < x + i; j++), returns from: return -1;\n" + + " \n" + + " Test next returns from: return cycles.loopInsideLoop(x);\n" + + " \n" + + "Test later returns from: return result;\n" + val summary3 = "Test calls InnerCalls::callLoopInsideLoop,\n" + + " there it calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + " iterates the loop for(int j = i; j < x + i; j++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): True\n" + + " returns from: return 1;\n" + + " Test next returns from: return cycles.loopInsideLoop(x);\n" + + " \n" + + "Test further returns from: return result;\n" + val summary4 = "Test calls InnerCalls::callLoopInsideLoop,\n" + + " there it calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) once,\n" + + " inside this loop, the test executes conditions:\n" + + " (i < 0): False\n" + + " iterates the loop for(int j = i; j < x + i; j++) twice,\n" + + " inside this loop, the test executes conditions:\n" + + " (j == 7): False\n" + + " (j == 7): True\n" + + " returns from: return 1;\n" + + " Test further returns from: return cycles.loopInsideLoop(x);\n" + + " \n" + + "Test then returns from: return result;\n" + val summary5 = "Test calls InnerCalls::callLoopInsideLoop,\n" + + " there it calls Cycles::loopInsideLoop,\n" + + " there it iterates the loop for(int i = x - 5; i < x; i++) 5 times. \n" + + " Test later does not iterate for(int j = i; j < x + i; j++), returns from: return -1;\n" + + " \n" + + " Test afterwards returns from: return cycles.loopInsideLoop(x);\n" + + " \n" + + "Test then returns from: return result;\n" + + val methodName1 = "testDoubleCallLoopInsideLoop_ILessThanZero" + val methodName2 = "testDoubleCallLoopInsideLoop_ReturnNegative1" + val methodName3 = "testDoubleCallLoopInsideLoop_JEquals7" + val methodName4 = "testDoubleCallLoopInsideLoop_JNotEquals7" + val methodName5 = "testDoubleCallLoopInsideLoop_ReturnNegative1_1" + + val displayName1 = "i < 0 : True -> return 2" + val displayName2 = "loopInsideLoop -> return -1" + val displayName3 = "i < 0 : False -> return 1" + val displayName4 = "j == 7 : False -> return 1" + val displayName5 = "loopInsideLoop -> return -1" + + val method = InnerCalls::doubleCallLoopInsideLoop + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testInnerCallFib() { + val summary1 = "Test calls Recursion::fib,\n" + + " there it executes conditions:\n" + + " (n == 0): False,\n" + + " (n == 1): True\n" + + " returns from: return 1;\n" + + " \n" + + "Test next returns from: return r.fib(n);\n" + val summary2 = "Test calls Recursion::fib,\n" + + " there it executes conditions:\n" + + " (n == 0): True\n" + + " returns from: return 0;\n" + + " \n" + + "Test next returns from: return r.fib(n);\n" + val summary3 = "Test calls Recursion::fib,\n" + + " there it executes conditions:\n" + + " (n == 0): False,\n" + + " (n == 1): False\n" + + " triggers recursion of fib twice, returns from: return fib(n - 1) + fib(n - 2);\n" + + " \n" + + "Test next returns from: return r.fib(n);\n" + val summary4 = "Test calls Recursion::fib,\n" + + " there it executes conditions:\n" + + " (n < 0): True\n" + + " triggers recursion of fib once, \n" + + "Test throws IllegalArgumentException in: return r.fib(n);\n" + + val methodName1 = "testCallFib_NEquals1" + val methodName2 = "testCallFib_NEqualsZero" + val methodName3 = "testCallFib_NNotEquals1" + val methodName4 = "testCallFib_NLessThanZero" + + val displayName1 = "n == 1 : True -> return 1" + val displayName2 = "n == 0 : True -> return 0" + val displayName3 = "n == 1 : False -> return fib(n - 1) + fib(n - 2)" + val displayName4 = "return r.fib(n) : True -> ThrowIllegalArgumentException" + + val method = InnerCalls::callFib + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt new file mode 100644 index 0000000000..096aed7fc9 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/inner/SummaryNestedCallsTest.kt @@ -0,0 +1,67 @@ +package examples.inner + +import examples.SummaryTestCaseGeneratorTest +import org.junit.Ignore +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Tag +import org.utbot.examples.inner.NestedCalls +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.inner.InnerCalls +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryNestedCallsTest : SummaryTestCaseGeneratorTest( + NestedCalls::class, +) { + @Test + fun testInvokeExample() { + val summary1 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + + " there it catches exception:\n" + + " IndexOutOfBoundsException e\n" + + " returns from: return -3;\n" + + " \n" + + "Test next returns from: return exceptionExamples.initAnArray(n);\n" + val summary2 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + + " there it catches exception:\n" + + " NegativeArraySizeException e\n" + + " returns from: return -2;\n" + + " \n" + + "Test afterwards returns from: return exceptionExamples.initAnArray(n);" + val summary3 = "Test calls NestedCalls\$ExceptionExamples::initAnArray,\n" + + " there it returns from: return a[n - 1] + a[n - 2];\n" + + " \n" + + "Test next returns from: return exceptionExamples.initAnArray(n);\n" + + val methodName1 = "testCallInitExamples_CatchIndexOutOfBoundsException" + val methodName2 = "testCallInitExamples_CatchNegativeArraySizeException" + val methodName3 = "testCallInitExamples_ReturnN1OfAPlusN2OfA" + + val displayName1 = "Catch (IndexOutOfBoundsException e) -> return -3" + val displayName2 = "Catch (NegativeArraySizeException e) -> return -2" + val displayName3 = "initAnArray -> return a[n - 1] + a[n - 2]" + + val method = NestedCalls::callInitExamples + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernary.kt b/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernary.kt new file mode 100644 index 0000000000..893b223665 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/examples/ternary/SummaryTernary.kt @@ -0,0 +1,591 @@ +package examples.ternary + +import examples.SummaryTestCaseGeneratorTest +import org.junit.Ignore +import org.junit.jupiter.api.Disabled +import org.utbot.examples.ternary.Ternary +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.examples.inner.InnerCalls +import org.utbot.examples.inner.NestedCalls +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryTernary : SummaryTestCaseGeneratorTest( + Ternary::class, +) { + @Test + fun testMax() { + val summary1 = "Test executes conditions:\n" + + " (val1 >= val2): False\n" + + "returns from: return val1 >= val2 ? val1 : val2;\n" + val summary2 = "Test executes conditions:\n" + + " (val1 >= val2): True\n" + + "returns from: return val1 >= val2 ? val1 : val2;\n" + + val methodName1 = "testMax_Val1LessThanVal2" + val methodName2 = "testMax_Val1GreaterOrEqualVal2" + + val displayName1 = "val1 >= val2 : False -> return val1 >= val2 ? val1 : val2" + val displayName2 = "val1 >= val2 : True -> return val1 >= val2 ? val1 : val2" + + val method = Ternary::max + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testSimpleOperation() { + val summary1 = "Test returns from: return result;\n" + + val methodName1 = "testSimpleOperation_ReturnResult" + + val displayName1 = "-> return result" + + val method = Ternary::simpleOperation + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1 + ) + + val displayNames = listOf( + displayName1 + ) + + val methodNames = listOf( + methodName1 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testStringExpr() { + val summary1 = "Test executes conditions:\n" + + " (num > 10): True\n" + + "returns from: return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";\n" + val summary2 = "Test executes conditions:\n" + + " (num > 10): False,\n" + + " (num > 5): False\n" + + "returns from: return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";\n" + val summary3 = "Test executes conditions:\n" + + " (num > 10): False,\n" + + " (num > 5): True\n" + + "returns from: return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\";\n" + + val methodName1 = "testStringExpr_NumGreaterThan10" + val methodName2 = "testStringExpr_NumLessOrEqual5" + val methodName3 = "testStringExpr_NumGreaterThan5" + + val displayName1 = "num > 10 : True -> return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\"" + val displayName2 = "num > 5 : False -> return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\"" + val displayName3 = "num > 5 : True -> return num > 10 ? \"Number is greater than 10\" : num > 5 ? \"Number is greater than 5\" : \"Number is less than equal to 5\"" + + val method = Ternary::stringExpr + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testParse() { + val summary1 = "Test executes conditions:\n" + + " (input == null || input.equals(\"\")): False\n" + + "returns from: return value;\n" + val summary2 = "Test executes conditions:\n" + + " (input == null || input.equals(\"\")): True\n" + + "invokes:\n" + + " String::equals once\n" + + "returns from: return value;\n" + val summary3 = "Test executes conditions:\n" + + " (input == null || input.equals(\"\")): True,\n" + + " (input == null || input.equals(\"\")): False\n" + + "invokes:\n" + + " Integer::parseInt once\n" + + "\n" + + "throws NumberFormatException in: Integer.parseInt(input)\n" + + val methodName1 = "testParse_InputEqualsNullOrInputEquals" + val methodName2 = "testParse_InputNotEqualsNullOrInputEquals" + val methodName3 = "testParse_InputEqualsNullOrInputEquals_1" + + val displayName1 = "input == null || input.equals(\"\") : False -> return value" + val displayName2 = "input == null || input.equals(\"\") : True -> return value" + val displayName3 = "Integer.parseInt(input) : True -> ThrowNumberFormatException" + + val method = Ternary::parse + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + @Test + fun testMinValue() { + val summary1 = "Test executes conditions:\n" + + " ((a < b)): False\n" + + "returns from: return (a < b) ? a : b;\n" + val summary2 = "Test executes conditions:\n" + + " ((a < b)): True\n" + + "returns from: return (a < b) ? a : b;\n" + + val methodName1 = "testMinValue_AGreaterOrEqualB" + val methodName2 = "testMinValue_ALessThanB" + + val displayName1 = "a < b : False -> return (a < b) ? a : b" + val displayName2 = "a < b : True -> return (a < b) ? a : b" + + val method = Ternary::minValue + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testSubDelay() { + val summary1 = "Test executes conditions:\n" + + " (flag): False\n" + + "returns from: return flag ? 100 : 0;\n" + val summary2 = "Test executes conditions:\n" + + " (flag): True\n" + + "returns from: return flag ? 100 : 0;\n" + + val methodName1 = "testSubDelay_NotFlag" + val methodName2 = "testSubDelay_Flag" + + val displayName1 = "flag : False -> return flag ? 100 : 0" + val displayName2 = "flag : True -> return flag ? 100 : 0" + + val method = Ternary::subDelay + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testPlusOrMinus() { + val summary1 = "Test executes conditions:\n" + + " ((num1 > num2)): False\n" + + "returns from: return (num1 > num2) ? (num1 + num2) : (num1 - num2);\n" + val summary2 = "Test executes conditions:\n" + + " ((num1 > num2)): True\n" + + "returns from: return (num1 > num2) ? (num1 + num2) : (num1 - num2);\n" + + val methodName1 = "testPlusOrMinus_Num1LessOrEqualNum2" + val methodName2 = "testPlusOrMinus_Num1GreaterThanNum2" + + val displayName1 = "num1 > num2 : False -> return (num1 > num2) ? (num1 + num2) : (num1 - num2)" + val displayName2 = "num1 > num2 : True -> return (num1 > num2) ? (num1 + num2) : (num1 - num2)" + + val method = Ternary::plusOrMinus + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testLongTernary() { + val summary1 = "Test executes conditions:\n" + + " (num1 > num2): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;\n" + val summary2 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;\n" + val summary3 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): False\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : 3;\n" + + val methodName1 = "testLongTernary_Num1GreaterThanNum2" + val methodName2 = "testLongTernary_Num1EqualsNum2" + val methodName3 = "testLongTernary_Num1NotEqualsNum2" + + val displayName1 = "num1 > num2 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : 3" + val displayName2 = "num1 == num2 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : 3" + val displayName3 = "num1 == num2 : False -> return num1 > num2 ? 1 : num1 == num2 ? 2 : 3" + + val method = Ternary::longTernary + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testVeryLongTernary() { + val summary1 = "Test executes conditions:\n" + + " (num1 > num2): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;\n" + val summary2 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): False,\n" + + " (num2 > num3): False,\n" + + " (num2 == num3): False\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;\n" + val summary3 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;\n" + val summary4 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): False,\n" + + " (num2 > num3): False,\n" + + " (num2 == num3): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;\n" + val summary5 = "Test executes conditions:\n" + + " (num1 > num2): False,\n" + + " (num1 == num2): False,\n" + + " (num2 > num3): True\n" + + "returns from: return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5;\n" + + val methodName1 = "testVeryLongTernary_Num1GreaterThanNum2" + val methodName2 = "testVeryLongTernary_Num2NotEqualsNum3" + val methodName3 = "testVeryLongTernary_Num1EqualsNum2" + val methodName4 = "testVeryLongTernary_Num2EqualsNum3" + val methodName5 = "testVeryLongTernary_Num2GreaterThanNum3" + + val displayName1 = "num1 > num2 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5" + val displayName2 = "num2 == num3 : False -> return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5" + val displayName3 = "num1 == num2 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5" + val displayName4 = "num2 == num3 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5" + val displayName5 = "num2 > num3 : True -> return num1 > num2 ? 1 : num1 == num2 ? 2 : num2 > num3 ? 3 : num2 == num3 ? 4 : 5" + + val method = Ternary::veryLongTernary + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testMinMax() { + val summary1 = "Test executes conditions:\n" + + " (num1 > num2): False\n" + + "calls Ternary::minValue,\n" + + " there it executes conditions:\n" + + " ((a < b)): True\n" + + " returns from: return (a < b) ? a : b;\n" + + " \n" + + "Test then returns from: return a;\n" + val summary2 = "Test executes conditions:\n" + + " (num1 > num2): True\n" + + "calls Ternary::max,\n" + + " there it executes conditions:\n" + + " (val1 >= val2): True\n" + + " returns from: return val1 >= val2 ? val1 : val2;\n" + + " \n" + + "Test further returns from: return a;\n" + val summary3 = "Test executes conditions:\n" + + " (num1 > num2): False\n" + + "calls Ternary::minValue,\n" + + " there it executes conditions:\n" + + " ((a < b)): False\n" + + " returns from: return (a < b) ? a : b;\n" + + " \n" + + "Test next returns from: return a;\n" + + val methodName1 = "testMinMax_ALessThanB" + val methodName2 = "testMinMax_Val1GreaterOrEqualVal2" + val methodName3 = "testMinMax_AGreaterOrEqualB" + + val displayName1 = "a < b : True -> return (a < b) ? a : b" + val displayName2 = "val1 >= val2 : True -> return val1 >= val2 ? val1 : val2" + val displayName3 = "a < b : False -> return (a < b) ? a : b" + + val method = Ternary::minMax + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + + @Test + fun testIntFunc() { + val summary1 = "Test executes conditions:\n" + + " (num1 > num2): True\n" + + "invokes:\n" + + " Ternary::intFunc1 once\n" + + "returns from: return num1 > num2 ? intFunc1() : intFunc2();\n" + val summary2 = "Test executes conditions:\n" + + " (num1 > num2): False\n" + + "invokes:\n" + + " Ternary::intFunc2 once\n" + + "returns from: return num1 > num2 ? intFunc1() : intFunc2();\n" + + val methodName1 = "testIntFunc_Num1GreaterThanNum2" + val methodName2 = "testIntFunc_Num1LessOrEqualNum2" + + val displayName1 = "num1 > num2 : True -> return num1 > num2 ? intFunc1() : intFunc2()" + val displayName2 = "num1 > num2 : False -> return num1 > num2 ? intFunc1() : intFunc2()" + + val method = Ternary::intFunc + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testTernaryInTheMiddle() { + val summary1 = "Test executes conditions:\n" + + " (num2 > num3): True\n" + + "returns from: return max(num1 + 228, num2 > num3 ? num2 + 1 : num3 + 2) + 4;\n" + val summary2 = "Test executes conditions:\n" + + " (num2 > num3): False\n" + + "returns from: return max(num1 + 228, num2 > num3 ? num2 + 1 : num3 + 2) + 4;\n" + + val methodName1 = "testTernaryInTheMiddle_Num2GreaterThanNum3" + val methodName2 = "testTernaryInTheMiddle_Num2LessOrEqualNum3" + + val displayName1 = "num2 > num3 : True -> return max(num1 + 228, num2 > num3 ? num2 + 1 : num3 + 2) + 4" + val displayName2 = "num2 > num3 : False -> return max(num1 + 228, num2 > num3 ? num2 + 1 : num3 + 2) + 4" + + val method = Ternary::ternaryInTheMiddle + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2 + ) + + val displayNames = listOf( + displayName1, + displayName2 + ) + + val methodNames = listOf( + methodName1, + methodName2 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testTwoIfsOneLine() { + val summary1 = "Test executes conditions:\n" + + " (num1 > num2): False\n" + + "returns from: return a;\n" + val summary2 = "Test executes conditions:\n" + + " (num1 > num2): True,\n" + + " ((num1 - 10) > 0): False\n" + + "returns from: return a;\n" + val summary3 = "Test executes conditions:\n" + + " (num1 > num2): True,\n" + + " ((num1 - 10) > 0): True\n" + + "returns from: return a;\n" + + val methodName1 = "testTwoIfsOneLine_Num1LessOrEqualNum2" + val methodName2 = "testTwoIfsOneLine_Num1Minus10LessOrEqualZero" + val methodName3 = "testTwoIfsOneLine_Num1Minus10GreaterThanZero" + + val displayName1 = "num1 > num2 : False -> return a" + val displayName2 = "(num1 - 10) > 0 : False -> return a" + val displayName3 = "(num1 - 10) > 0 : True -> return a" + + val method = Ternary::twoIfsOneLine + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryIntMath.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMath.kt new file mode 100644 index 0000000000..43b3520970 --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryIntMath.kt @@ -0,0 +1,144 @@ +package math + +import examples.SummaryTestCaseGeneratorTest +import guava.examples.math.IntMath +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.framework.plugin.api.MockStrategyApi + +class SummaryIntMath : SummaryTestCaseGeneratorTest( + IntMath::class, +) { + @Test + fun testPow() { + val summary1 = "Test activates switch case: 2, returns from: return 1;\n" + val summary2 = "Test executes conditions:\n" + + " (k < Integer.SIZE): False\n" + + "returns from: return 0;\n" + val summary3 = "Test executes conditions:\n" + + " ((k < Integer.SIZE)): False\n" + + "returns from: return (k < Integer.SIZE) ? (1 << k) : 0;\n" + val summary4 = "Test iterates the loop for(int accum = 1; ; k >>= 1) once,\n" + + " inside this loop, the test returns from: return b * accum;" + val summary5 = "Test executes conditions:\n" + + " ((k < Integer.SIZE)): True\n" + + "returns from: return (k < Integer.SIZE) ? (1 << k) : 0;\n" + val summary6 = "Test executes conditions:\n" + + " ((k == 0)): False\n" + + "returns from: return (k == 0) ? 1 : 0;\n" + val summary7 = "Test iterates the loop for(int accum = 1; ; k >>= 1) once,\n" + + " inside this loop, the test returns from: return accum;" + val summary8 = "Test executes conditions:\n" + + " ((k == 0)): True\n" + + "returns from: return (k == 0) ? 1 : 0;\n" + val summary9 = "Test executes conditions:\n" + + " (k < Integer.SIZE): True,\n" + + " (((k & 1) == 0)): True\n" + + "returns from: return ((k & 1) == 0) ? (1 << k) : -(1 << k);\n" + val summary10 = "Test executes conditions:\n" + + " (k < Integer.SIZE): True,\n" + + " (((k & 1) == 0)): False\n" + + "returns from: return ((k & 1) == 0) ? (1 << k) : -(1 << k);\n" + val summary11 = "Test executes conditions:\n" + + " (((k & 1) == 0)): False\n" + + "returns from: return ((k & 1) == 0) ? 1 : -1;\n" + val summary12 = "Test executes conditions:\n" + + " (((k & 1) == 0)): True\n" + + "returns from: return ((k & 1) == 0) ? 1 : -1;\n" + val summary13 = "Test iterates the loop for(int accum = 1; ; k >>= 1) twice,\n" + + " inside this loop, the test executes conditions:\n" + + " (((k & 1) == 0)): False\n" + + "returns from: return b * accum;" + val summmary14 = "Test iterates the loop for(int accum = 1; ; k >>= 1) twice,\n" + + " inside this loop, the test executes conditions:\n" + + " (((k & 1) == 0)): True\n" + + "returns from: return b * accum;" + + val methodName1 = "testPow_Return1" + val methodName2 = "testPow_KGreaterOrEqualIntegerSIZE" + val methodName3 = "testPow_KGreaterOrEqualIntegerSIZE_1" + val methodName4 = "testPow_ReturnBMultiplyAccum" + val methodName5 = "testPow_KLessThanIntegerSIZE" + val methodName6 = "testPow_KNotEqualsZero" + val methodName7 = "testPow_ReturnAccum" + val methodName8 = "testPow_KEqualsZero" + val methodName9 = "testPow_KBitwiseAnd1EqualsZero" + val methodName10 = "testPow_KBitwiseAnd1NotEqualsZero" + val methodName11 = "testPow_KBitwiseAnd1NotEqualsZero_1" + val methodName12 = "testPow_KBitwiseAnd1EqualsZero_1" + val methodName13 = "testPow_KBitwiseAnd1NotEqualsZero_2" + val methodName14 = "testPow_KBitwiseAnd1EqualsZero_2" + + val displayName1 = "switch(b) case: 2 -> return 1" + val displayName2 = "k < Integer.SIZE : False -> return 0" + val displayName3 = "k < Integer.SIZE : False -> return (k < Integer.SIZE) ? (1 << k) : 0" + val displayName4 = "-> return b * accum" // TODO: weird display name with missed part before -> + val displayName5 = "k < Integer.SIZE : True -> return (k < Integer.SIZE) ? (1 << k) : 0" + val displayName6 = "k == 0 : False -> return (k == 0) ? 1 : 0" + val displayName7 = "-> return accum" // TODO: weird display name with missed part before -> + val displayName8 = "k == 0 : True -> return (k == 0) ? 1 : 0" + val displayName9 = "(k & 1) == 0 : True -> return ((k & 1) == 0) ? (1 << k) : -(1 << k)" + val displayName10 = "(k & 1) == 0 : False -> return ((k & 1) == 0) ? (1 << k) : -(1 << k)" + val displayName11 = "(k & 1) == 0 : False -> return ((k & 1) == 0) ? 1 : -1" + val displayName12 = "(k & 1) == 0 : True -> return ((k & 1) == 0) ? 1 : -1" + val displayName13 = "(k & 1) == 0 : False -> return b * accum" + val displayName14 = "(k & 1) == 0 : True -> return b * accum" + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6, + summary7, + summary8, + summary9, + summary10, + summary11, + summary12, + summary13, + summmary14 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6, + displayName7, + displayName8, + displayName9, + displayName10, + displayName11, + displayName12, + displayName13, + displayName14 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7, + methodName8, + methodName9, + methodName10, + methodName11, + methodName12, + methodName13, + methodName14 + ) + + val method = IntMath::pow + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary-tests/src/test/kotlin/math/SummaryOfMath.kt b/utbot-summary-tests/src/test/kotlin/math/SummaryOfMath.kt new file mode 100644 index 0000000000..322cf5ff4f --- /dev/null +++ b/utbot-summary-tests/src/test/kotlin/math/SummaryOfMath.kt @@ -0,0 +1,215 @@ +package math + +import examples.SummaryTestCaseGeneratorTest +import guava.examples.math.Stats +import org.junit.jupiter.api.Test +import org.utbot.examples.DoNotCalculate +import org.utbot.framework.plugin.api.MockStrategyApi + +/** + * It runs test generation for the poor analogue of the Stats.of method ported from the guava-26.0 framework + * and validates generated docs, display names and test method names. + * + * @see Related issue + */ +class SummaryOfMath : SummaryTestCaseGeneratorTest( + Stats::class, +) { + @Test + fun testOfInts() { + val summary1 = "Test calls StatsAccumulator::addAll,\n" + + " there it triggers recursion of addAll once, \n" + + "Test throws NullPointerException in: acummulator.addAll(values);\n" + val summary2 = "Test calls StatsAccumulator::addAll,\n" + + " there it does not iterate for(int value: values), \n" + + "Test later calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test then returns from: return acummulator.snapshot();" + val summary3 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(int value: values) once. \n" + + "Test later calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test then returns from: return acummulator.snapshot();" + val summary4 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(int value: values) twice. \n" + + "Test later calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test later returns from: return acummulator.snapshot();\n" + + val methodName1 = "testOfInts_StatsAccumulatorAddAll" + val methodName2 = "testOfInts_snapshot" + val methodName3 = "testOfInts_IterateForEachLoop" + val methodName4 = "testOfInts_IterateForEachLoop_1" + + val displayName1 = "acummulator.addAll(values) : True -> ThrowNullPointerException" + val displayName2 = "snapshot -> return new Stats(count, mean, sumOfSquaresOfDeltas, min, max)" + val displayName3 = "addAll -> return new Stats(count, mean, sumOfSquaresOfDeltas, min, max)" + val displayName4 = "addAll -> return new Stats(count, mean, sumOfSquaresOfDeltas, min, max)" + + val method = Stats::ofInts + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } + + @Test + fun testOfDoubles() { + val summary1 = "Test calls StatsAccumulator::addAll,\n" + + " there it triggers recursion of addAll once, \n" + + "Test throws NullPointerException in: acummulator.addAll(values);\n" + val summary2 = "Test calls StatsAccumulator::addAll,\n" + + " there it does not iterate for(double value: values), \n" + + "Test next calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test later returns from: return acummulator.snapshot();\n" + val summary3 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(double value: values) twice,\n" + + " inside this loop, the test calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (count == 0): True\n" + + " (!isFinite(value)): True\n" + + " calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (count == 0): False\n" + + " (isFinite(value) && isFinite(mean)): True\n" + + " (if (isFinite(value) && isFinite(mean)) {\n" + + " double delta = value - mean;\n" + + " mean += delta / count;\n" + + " sumOfSquaresOfDeltas += delta * (value - mean);\n" + + "} else {\n" + + " mean = calculateNewMeanNonFinite(mean, value);\n" + + " sumOfSquaresOfDeltas = NaN;\n" + + "}): False\n" + + "Test afterwards calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test afterwards returns from: return acummulator.snapshot();\n" + val summary4 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(double value: values) twice,\n" + + " inside this loop, the test calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (!isFinite(value)): False\n" + + "Test next calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test then returns from: return acummulator.snapshot();\n" + val summary5 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(double value: values) twice,\n" + + " inside this loop, the test calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (count == 0): True\n" + + " (!isFinite(value)): False\n" + + " calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (count == 0): False\n" + + " (isFinite(value) && isFinite(mean)): True\n" + + " (if (isFinite(value) && isFinite(mean)) {\n" + + " double delta = value - mean;\n" + + " mean += delta / count;\n" + + " sumOfSquaresOfDeltas += delta * (value - mean);\n" + + "} else {\n" + + " mean = calculateNewMeanNonFinite(mean, value);\n" + + " sumOfSquaresOfDeltas = NaN;\n" + + "}): True\n" + + "Test later calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test then returns from: return acummulator.snapshot();\n" + val summary6 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(double value: values) twice,\n" + + " inside this loop, the test calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (!isFinite(value)): True\n" + + "Test afterwards calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test then returns from: return acummulator.snapshot();\n" + val summary7 = "Test calls StatsAccumulator::addAll,\n" + + " there it iterates the loop for(double value: values) twice,\n" + + " inside this loop, the test calls StatsAccumulator::add,\n" + + " there it executes conditions:\n" + + " (!isFinite(value)): True\n" + + "Test later calls StatsAccumulator::snapshot,\n" + + " there it returns from: return new Stats(count, mean, sumOfSquaresOfDeltas, min, max);\n" + + " \n" + + "Test further returns from: return acummulator.snapshot();\n" + + val methodName1 = "testOfDoubles_StatsAccumulatorAddAll" + val methodName2 = "testOfDoubles_snapshot" + val methodName3 = "testOfDoubles_IsFiniteAndIsFinite" + val methodName4 = "testOfDoubles_IsFinite" + val methodName5 = "testOfDoubles_IsFiniteAndIsFinite_1" + val methodName6 = "testOfDoubles_NotIsFinite" + val methodName7 = "testOfDoubles_NotIsFinite_1" + + val displayName1 = "acummulator.addAll(values) : True -> ThrowNullPointerException" + val displayName2 = "snapshot -> return new Stats(count, mean, sumOfSquaresOfDeltas, min, max)" + val displayName3 = "!isFinite(value) : True -> StatsAccumulatorCalculateNewMeanNonFinite" + val displayName4 = "add -> !isFinite(value) : False" + val displayName5 = "!isFinite(value) : False -> isFinite(value) && isFinite(mean)" + val displayName6 = "add -> !isFinite(value) : True" + val displayName7 = "add -> !isFinite(value) : True" + + val method = Stats::ofDoubles + val mockStrategy = MockStrategyApi.NO_MOCKS + val coverage = DoNotCalculate + + val summaryKeys = listOf( + summary1, + summary2, + summary3, + summary4, + summary5, + summary6, + summary7 + ) + + val displayNames = listOf( + displayName1, + displayName2, + displayName3, + displayName4, + displayName5, + displayName6, + displayName7 + ) + + val methodNames = listOf( + methodName1, + methodName2, + methodName3, + methodName4, + methodName5, + methodName6, + methodName7 + ) + + check(method, mockStrategy, coverage, summaryKeys, methodNames, displayNames) + } +} \ No newline at end of file diff --git a/utbot-summary/build.gradle b/utbot-summary/build.gradle index d0d8ff1c2e..020fef96c2 100644 --- a/utbot-summary/build.gradle +++ b/utbot-summary/build.gradle @@ -1,11 +1,9 @@ apply from: "${parent.projectDir}/gradle/include/jvm-project.gradle" -apply plugin: "java" - dependencies { implementation "com.github.UnitTestBot:soot:${soot_commit_hash}" api project(':utbot-framework-api') - implementation(project(':utbot-instrumentation')) + compile(project(':utbot-instrumentation')) implementation group: 'com.github.haifengl', name: 'smile-kotlin', version: '2.6.0' implementation group: 'com.github.haifengl', name: 'smile-core', version: '2.6.0' @@ -13,4 +11,4 @@ dependencies { implementation group: 'io.github.microutils', name: 'kotlin-logging', version: kotlin_logging_version implementation group: 'com.github.javaparser', name: 'javaparser-core', version: '3.22.1' -} \ No newline at end of file +} diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt index 0f62518907..fb473101e5 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/Summarization.kt @@ -68,12 +68,32 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List() val clustersToReturn = mutableListOf() + // TODO: Now it excludes tests generated by Fuzzer, handle it properly, related to the https://github.com/UnitTestBot/UTBotJava/issues/428 + val executionsProducedByFuzzer = getExecutionsWithEmptyPath(testCase) + + if (executionsProducedByFuzzer.isNotEmpty()) { + executionsProducedByFuzzer.forEach { + logger.info { + "The path for test ${it.testMethodName} " + + "for method ${testCase.method.clazz.qualifiedName} is empty and summaries could not be generated." + } + } + + clustersToReturn.add( + UtExecutionCluster( + UtClusterInfo(), + executionsProducedByFuzzer + ) + ) + } + // analyze if (jimpleBody != null && sootToAST != null) { val methodUnderTest = jimpleBody.method @@ -83,9 +103,9 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List 1 //there is more than one successful execution - && clusterTraceTags.traceTags.size > 1 //add if there is more than 1 execution + GENERATE_CLUSTER_COMMENTS && clusterTraceTags.isSuccessful // add only for successful executions + && numberOfSuccessfulClusters > 1 // there is more than one successful execution + && clusterTraceTags.traceTags.size > 1 // add if there is more than 1 execution ) { SimpleClusterCommentBuilder(clusterTraceTags.commonStepsTraceTag, sootToAST) .buildString(methodUnderTest) @@ -144,6 +164,9 @@ class Summarization(val sourceFile: File?, val invokeDescriptions: List() - //we only want to find intersections if there is more than one successful execution + // we only want to find intersections if there is more than one successful execution if (numberOfSuccessfulClusters > 1 && REMOVE_INTERSECTIONS) { val commonStepsInSuccessfulEx = listOfSplitSteps .filterIndexed { i, _ -> clusteredExecutions[i] is SuccessfulExecutionCluster } //search only in successful @@ -46,7 +46,7 @@ class TagGenerator { } } - //for every cluster and step add TraceTagCluster + // for every cluster and step add TraceTagCluster clusteredExecutions.zip(listOfSplitSteps) { cluster, splitSteps -> val commonStepsInCluster = if (stepsIntersections.isNotEmpty() && numberOfSuccessfulClusters > 1) { @@ -70,11 +70,10 @@ class TagGenerator { ) ) } - }//clusteredExecutions should not be empty! + } // clusteredExecutions should not be empty! return traceTagClusters } - } /** @@ -95,7 +94,7 @@ private fun generateExecutionTags(executions: List, splitSteps: Spl * @return clustered executions */ private fun toClusterExecutions(testCase: UtTestCase): List { - val methodExecutions = testCase.executions + val methodExecutions = testCase.executions.filter { it.path.isNotEmpty() } // TODO: Now it excludes tests generated by Fuzzer, handle it properly, related to the https://github.com/UnitTestBot/UTBotJava/issues/428 val clusters = mutableListOf() val commentPostfix = "for method ${testCase.method.displayName}" diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/ast/JimpleToASTMap.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/ast/JimpleToASTMap.kt index 3a4c048e85..13b0ed1673 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/ast/JimpleToASTMap.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/ast/JimpleToASTMap.kt @@ -61,7 +61,8 @@ class JimpleToASTMap(stmts: Iterable, methodDeclaration: MethodDeclaration if (ASTNode != null) { if (ASTNode is IfStmt && stmt is JIfStmt) { - ASTNode = ifStmtToNodeMap[ASTNode]?.remove() + val nodes = ifStmtToNodeMap[ASTNode] + if(!nodes.isNullOrEmpty()) ASTNode = nodes.remove() } else if (stmt is JReturnStmt) { ASTNode = validateReturnASTNode(ASTNode) } diff --git a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt index 2b0129a264..0caa0eaf42 100644 --- a/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt +++ b/utbot-summary/src/main/kotlin/org/utbot/summary/comment/SimpleSentenceBlock.kt @@ -433,7 +433,7 @@ data class SquashedStmtTexts( for (i in 0 until stmtTexts.size) { val stmtText = stmtTexts[i] if ((tab + stmtText.prefix).isNotEmpty()) stmts += DocRegularStmt(tab + stmtText.prefix) - stmts += DocCodeStmt(stmtText.description) + stmts += DocCodeStmt(stmtText.description.replace(CARRIAGE_RETURN, "")) if (i != stmtTexts.lastIndex) { if (separation.isNotEmpty()) stmts += DocRegularStmt(separation)